# AdAdapted SDK Documentation > AdAdapted provides SDKs for integrating native advertising into mobile and web applications. SDKs are available for React Native, JavaScript, Flutter, Android, iOS (Swift Package and CocoaPods), and the Payload as a Service platform. Features include ad zone creation, in-app ad display, keyword intercept, add-to-list/cart actions, out-of-app payloads, contextual recipe ads, off-screen ad zone visibility, and event tracking. Version notes name the release that introduced a change and stay true for every later release: in the React Native SDK, v5.0.0 moved to one ad per zone per request with the host app rendering an AdZone component per zone, and upgrading from 4.x is a breaking change; in the JavaScript SDK, v3.0.0 moved to one ad per zone per request and client generated sessions, and upgrading from 2.x is a breaking change; the Flutter SDK is new at v0.1.2 and follows the same single ad serving model, with the host app placing an AdZone widget per zone and each zone measuring its own visibility rather than being told; in the Android SDK, onZoneHasAds() reports on every zone response as of v6.0.0 and AaSdkSessionListener was removed in v5.0.0 in favor of per-zone listeners; the iOS Swift Package SDK carries those same two changes as of v4.0.0 and v3.0.0 respectively, and ships Objective-C compatibility wrappers so Objective-C apps use it without rewriting in Swift. These docs are updated only for releases that bring a breaking change or a new feature, so the newest version named here is not necessarily the latest release. ## React Native SDK ### Getting Started # Getting Started > **Note:** As of v5.0.0, ad zones are rendered by your app as AdZone components, one per zone, rather than read back from the SDK. This is a breaking change, and any implementation on 4.x requires a migration — start with the Upgrade Guide. ## Features - Ad zones you place yourself, one ad per zone, each refreshing on its own schedule - In-list add suggestion based on available keywords - Event tracking enabling reporting - Supported ad types: - Add to list (reports a product to add to a list) - Standard external link (opens in external browser) - Off-screen ad zones - Contextual recipe ads > **Note:** The two in-app popup ad types — the add to list circular and the popup external link — are not displayed by this SDK. An ad served with either action type stays on screen and does nothing when tapped. This is long-standing rather than new in v5.0.0. See Caveats. ## Installation ```bash npm install --save @adadapted/react-native-sdk ``` The latest release is **v5.0.0**. To pin to it explicitly: ```bash npm install --save @adadapted/react-native-sdk@^5.0.0 ``` Once the NPM install script above is complete, navigate to your `/ios` directory in your project and run the following: ```bash pod install ``` This will ensure that iOS is linked up to the AdAdapted SDK dependency properly. > **Note:** The SDK depends on react-native-get-random-values, which provides the secure random source used to generate session IDs. It is installed for you as a dependency, but it ships a native module, so the pod install step above is required on iOS. ## License [AdAdapted Platform License](docs/license.md) --- ### Usage # Usage > **Note:** This page describes v5.0.0, in which ad zones are rendered by your app rather than read back from the SDK. If you are coming from 4.x, start with the Upgrade Guide — v5.0.0 is a breaking change. The following is an example of how to use the SDK. Start by importing the `AdadaptedReactNativeSdk` class. You should do this in your main component file, so you are defining the SDK reference once and passing down the reference as needed through props. ```js import { AdadaptedReactNativeSdk } from "@adadapted/react-native-sdk"; ``` --- When using class components, assign a reference to the SDK within the component's `constructor()`. When using functional components, you can use the `useMemo()` hook to create an instance of the SDK. #### ** Class ** ```js constructor(props) { super(props); this.aaSdk = new AdadaptedReactNativeSdk(); } ``` #### ** Function ** ```js const aaSdk = useMemo(() => { return new AdadaptedReactNativeSdk(); }, []); ``` > **Note:** There is no longer any ad zone list to hold in component state. Each zone is a component your app renders, and it owns its own data. --- For class components, trigger the SDK initialization by calling the `initialize()` method on your SDK reference within the `componentDidMount()` react lifecycle method. When using functional components, trigger the SDK initialization by calling the `initialize()` method in a `useEffect()` hook with an empty dependency array. In the below examples, you will need to replace the string `YOUR_APP_ID` with the app ID provided to you by AdAdapted. The `initialize()` method is promise based, so once the `.then()` callback occurs, the SDK is initialized and you can start interfacing as needed with your reference of the SDK. You can provide a definition for the `onAddToListTriggered()` method if you would like to use the `add to list` feature of the SDK. See [Callbacks: onAddToListTriggered()](docs/react-native-callbacks?id=onaddtolisttriggered) for more details. #### ** Class ** ```js componentDidMount(): void { this.aaSdk .initialize({ appId: "YOUR_APP_ID", onAddToListTriggered: (items) => { for (const item of items) { // Add the item to the users list. } } }) .then(() => { // SDK session has been created. this.setState({ sessionId: this.aaSdk.getSessionId() }); }) .catch((err) => { console.error(err); }); } ``` #### ** Function ** ```js useEffect(() => { aaSdk .initialize({ appId: "YOUR_APP_ID", onAddToListTriggered: (items) => { // Demonstrate adding all provided items to the // client side list. for (const item of items) { selectItem({ itemName: item.product_title, }); } }, }) .then(() => { // SDK session has been created. The session is generated // locally, so it is available as soon as this resolves. setSessionId(aaSdk.getSessionId()); }) .catch((err) => { console.error(err); }); return () => { // Unmount the SDK. if (aaSdk) { aaSdk.unmount(); } }; }, []); ``` --- You will want to make sure to clean up the reference to the SDK when you unmount your component. To do this, call the `unmount()` method on your SDK reference from within the `componentWillUnmount()` react lifecycle method for classes (below) or in a `useEffect()` for functional components (see example above). This will ensure certain features of the SDK are cleaned up and will allow you to avoid memory leaks. ```js componentWillUnmount(): void { if (this.aaSdk) { this.aaSdk.unmount(); } } ``` --- ## Rendering Ad Zones Render an `AdZone` for each zone AdAdapted has allocated to you, wherever that zone belongs in your layout. The zone owns its own ad request, its own refresh and its own impression reporting, so there is nothing to wire up beyond placing it. `isVisible` is required — React Native gives the SDK no way to detect whether a zone is actually in front of the user, so your app reports it. #### ** Class ** ```jsx render() { return ( ); } ``` #### ** Function ** ```jsx const isFocused = useIsFocused(); return ( ); ``` > **Note:** The container you place a zone in must have a size. The zone fills the space you give it, so a container with no height renders nothing visible. See [Ad Zones](docs/react-native-ad-zones.md) for the full component reference — visibility reporting, recipe contexts, per-zone callbacks and off-screen zones. --- ### Ad Zones # Ad Zones Ad zones are rendered by your app. For each zone AdAdapted has allocated to you, render an `AdZone` component and give it the zone ID. The component owns everything else about that zone: its own ad request, its own refresh countdown and its own impression reporting. > **Note:** As of v5.0.0 this replaces the retired getAdZones() / AdZoneInfo model, where the SDK built the zone components and handed them back to you. See the Upgrade Guide if you are migrating from 4.x. ## Rendering a Zone Import `AdZone` alongside the SDK class and place one per zone, wherever that zone belongs in your layout. ```jsx import { AdZone } from "@adadapted/react-native-sdk"; ; ``` > **Note:** The container you place the zone in must have a size. The zone fills the space you give it, so a container with no height renders nothing visible. The zone starts requesting ads once the SDK has finished initializing, so it is safe to render before `initialize()` resolves. --- ## Reporting Visibility `isVisible` is **required**. React Native gives the SDK no way to work out whether a zone is actually in front of the user, so your app has to report it. While `isVisible` is false the zone neither refreshes nor records impressions. > **Note:** This prop is required as of v5.0.0, rather than defaulted, because both defaults fail quietly. Defaulting to true over-reports impressions for a zone left mounted on a screen the user has navigated away from. Defaulting to false is worse: the ad is still fetched and still shown to the user, but no impression is ever reported and the zone never refreshes — so a zone whose prop you forgot would serve ads and report nothing, indefinitely and without an error. For a zone that really is always on screen, pass `true` explicitly: ```jsx ``` ### Zones on a Navigation Stack A native stack keeps a screen mounted when another screen is pushed on top of it. The zone is no longer in front of the user, but nothing about the component changed, so you have to say so. `useIsFocused()` from React Navigation reports exactly this: ```jsx import { useIsFocused } from "@react-navigation/native"; const isFocused = useIsFocused(); ; ``` ### Zones That Can Be Scrolled Off Screen For a zone inside a scroll view, report whether it is currently scrolled into view. Any intersection-observer style library works; the example app uses `react-native-intersection-observer`: ```jsx import { IOScrollView, InView } from "react-native-intersection-observer"; const [isInView, setIsInView] = useState(false); const isFocused = useIsFocused(); {/* ...other content... */} ; ``` > **Note:** Scroll position alone is not enough. An intersection observer keeps reporting a zone as in view when another screen is pushed over it, because nothing scrolled. Combine it with the screen's focus state, as above. --- ## Contextual Ads (Recipes) Recipe context is set per zone, using the `contextId` prop. Set it to the context the zone is currently showing, and clear it by passing `undefined`. ```jsx const [adContextId, setAdContextId] = useState(undefined); ; ``` > **Note:** As of v5.0.0 this replaces the SDK-wide setAdContext() and clearAdContext() methods, which could not express a different context for different zones on the same screen. --- ## Props | Prop | Type | Required | Description | | ----------------------- | ----------------------------- | -------- | -------------------------------------------------------------------------------------------------------- | | `zoneId` | `string` | Yes | The zone ID provided to you by AdAdapted. | | `isVisible` | `boolean` | Yes | Whether the zone is currently on screen. While false the zone does not refresh or record impressions. | | `contextId` | `string` | No | The recipe context this zone is currently showing. Pass `undefined` to clear it. | | `xyDragDistanceAllowed` | `number` | No | Touch drag distance, in either direction, still treated as a tap rather than a scroll. | | `style` | `StyleProp` | No | Style applied to the zone's outer `View`. | | `onAddToListTriggered` | `(items) => void` | No | Triggered when an `add to list` ad in this zone is tapped. | | `onZoneHasAds` | `(hasAds: boolean) => void` | No | Called whenever the zone's fill state changes, so you can collapse or reveal the space around it. | | `onAdLoaded` | `() => void` | No | Called when an ad has been retrieved and displayed. | | `onAdLoadFailed` | `() => void` | No | Called when an ad could not be retrieved or displayed. | --- ## Zone Callbacks ### onAddToListTriggered() Triggered when a user taps an `add to list` ad in this zone. Receives the list of items the user selected to add. ```jsx { for (const item of items) { // Add the item to the user's list. } }} /> ``` > **Note:** You can handle this per zone here, or once for every zone through the onAddToListTriggered() callback on initialize(). Use whichever suits your app; the per-zone prop is useful when different zones feed different lists. See [Callbacks: onAddToListTriggered()](docs/react-native-callbacks?id=onaddtolisttriggered) for the `items` type definition. > **Note:** After the items reach the user's list, call acknowledge() on your SDK reference for each item, so the interaction is reported. ### onZoneHasAds() Called whenever the zone's fill state changes. Use it to collapse the space around a zone that has nothing to show. ```jsx const [zoneHasAds, setZoneHasAds] = useState(false); ; ``` ### onAdLoaded() and onAdLoadFailed() `onAdLoaded()` is called when an ad has been retrieved and displayed. `onAdLoadFailed()` is called when an ad could not be retrieved or displayed. ```jsx { // An ad is now on screen in this zone. }} onAdLoadFailed={() => { // Nothing could be shown for this zone. }} /> ``` --- ## Refreshing Each zone refreshes on its own schedule, using the refresh time served with its current ad. There is nothing to call and nothing to keep in state — the zone swaps its own ad when the countdown elapses. The countdown is paused while `isVisible` is false and while the app is backgrounded, so a zone the user cannot see does not burn through ads. --- ### Methods # Methods ## initialize() The initialize method is where your SDK session is created. This method returns a promise that when complete, your reference to the SDK will have a valid SDK session going forward. Once you have a valid session, you will be able to successfully use other SDK methods. You should call this method from within the `componentDidMount()` lifecycle method for clases or a `useMemo()` hook for functions react to ensure it is only called once upon component mount. ```js aaSdk.initialize({ /** * The app ID provided to you by AdAdapted. * If you have multiple variations of the app, you will be provided with multiple App IDs. * For each App ID you have, you will need to determine the correct one to provide the SDK when initializing based on the users variation of the app. * Must be provided for the SDK to initialize. */ appId: string, /** (Optional) * The unique ID to use during initialization. * This doesn't have to be the advertiser ID. You can provide any ID that you would like to uniquely identify a user. */ advertiserId: string, /** * (Optional) * Provide this value to define what API environment the SDK should use. * * Possible Values: * - Production: EnvironmentTypes.ApiEnv.Prod * - Development: EnvironmentTypes.ApiEnv.Dev * * Production is the default value if you don't provide this property in your config. */ apiEnv: EnvironmentTypes.ApiEnv, /** * (Optional) * Adjusts the touch sensitivity of the Ad Zone in both the X and Y directions. * This is used to determine the click/press sensitivity when the * Ad Zone is being touched by the user as a regular touch or while * scrolling the view. If the amount of touch "drag" distance in either * X or Y direction is less than this value, we will treat the action as * a click/press on the Ad Zone. * * This value represents the number of pixels from start to finish of a user * draging their finger after touching an Ad Zone. If the total drag distance * exceeds this value, the click/press of the Ad Zone will not be triggered. * The higher the value set to this property, the easier it is for a user to * trigger the Ad click/press. * * If not set, the default for this property is 25. */ xyDragDistanceAllowed: number, /** * (Optional) * The store to target ads for, if you are targeting ads by store. */ storeId: string, /** * (Optional) * Triggered when an "add to list" action has * taken place by a user, in any ad zone. */ onAddToListTriggered: (items) => {}, /** * (Optional) * Callback that gets triggered when an "add to list" * occurs by means of an "out of app" data payload. */ onOutOfAppPayloadAvailable: (payloads) => {}, }) ``` **Return Type:** ```js Promise ``` > **Note:** Ad zones are not configured here. Render an AdZone component for each zone AdAdapted has allocated to you, and give it the zone ID. Recipe contexts and zone visibility are per-zone props on that component. --- ## unmount() This method will ensure the SDK is cleaned up when the component it is defined within is being destroyed. This method must be called within the `componentWillUnmount()` react lifecycle method for classes, or within the return block of a non-dependency `useEffect()` for functional components. > **Note:** If you do not call this method, you risk creating a memory leak. ```js aaSdk.unmount(); ``` **Return Type:** ```js void ``` --- ## getSessionId() Once the SDK is initialized, this method returns the current session ID. The session is generated on the client, so the ID is available as soon as `initialize()` resolves. > **Note:** As of v5.0.0, sessions are held in memory and never persisted. A relaunch always starts a new session, and only returning to the foreground within 30 minutes resumes the existing one. ```js aaSdk.getSessionId(); ``` **Return Type:** ```js string | undefined ``` --- ## getDeviceInfo() Once the SDK is initialized, this method exposes device information based on the users current device. ```js aaSdk.getDeviceInfo(); ``` **Return Type:** ```js { /** * The unique device ID. */ udid: string; /** * The device name. */ deviceName: string; /** * The operating system name. */ systemName: string; /** * The operating system version. */ systemVersion: string; /** * The device model. */ deviceModel: string; /** * The device screen width. */ deviceWidth: string; /** * The device screen height. */ deviceHeight: string; /** * The device screen density. */ deviceScreenDensity: string; /** * The current device local. */ deviceLocale: string; /** * The bundle ID. */ bundleId: string; /** * The bundle version. */ bundleVersion: string; /** * The current device timezone. */ deviceTimezone: string; /** * If true, ad tracking is enabled for the device. */ isAdTrackingEnabled: boolean; } ``` --- ## acknowledge() This method must be called after user interaction with an ad zone for proper impression tracking. This method should be called after the item has been added to the user's shopping list. > **Note:** As of v5.0.0 the ad's own interaction is reported by the zone, once, on a real tap. Previously it was reported on touch start, so a scroll that began on an ad counted as a click and this method then reported a second one for the same ad. ```js aaSdk.acknowledge(itemName: string); ``` **Return Type:** ```js void ``` --- ## performKeywordSearch() Once the SDK is initialized, this method can be called when performing a search operation within your app. Call this method either as the user types in a search term or when the user executes a search after the term is entered. A list of keyword search results will be returned by this method that can then be used to display dynamically along with the search results your app displays to the user. The final result of the search may have many items, in which case you would determine how many of them you would like to display. ```js aaSdk.performKeywordSearch(searchTerm: string); ``` > **Note:** Matching happens entirely on the device. The SDK downloads the list of available keyword terms when it initializes, and performKeywordSearch checks the user's input against that local list. No network request is needed to find a match, so results come back instantly and can be displayed to the user as they type. > > For the purposes of campaign reporting, performKeywordSearch does send a lightweight background event recording the search term and whether it matched. Because search terms are reported, include Search History in your App Store privacy details and in-app search history in Google Play's Data safety form, if not doing so already. **Return Type:** ```js [ { /** * The search term ID. */ term_id: string; /** * The search term that was used to validate * the provided search string against. */ term: string; /** * The display string you can use to display the * resulting item in your search results list. */ replacement: string; /** * The display priority of this item amongst all * other keyword search results. The priority acts * as a secondary sort. The primary sort is based * on whether the "term" starts with the provided * search term characters or not. */ priority: number; } ] ``` > **Note:** Sorting based on priority is already handled for you by the SDK. The results will already be in the correct sort order. The final result of suggestions is pre-sorted based on the following criteria: - First, the suggestions are sorted based on which terms "start" with the characters in the provided search string. - Second, they are then sorted by a pre-determined priority for each keyword that satisfied the search. **Search Example:** _che_ | Keyword Term | Priority | | ------------------- | -------- | | **Che**x Mix | 2 | | **Che**ddar Cheese | 3 | | Sweet **Che**rries | 1 | | Mac & **Che**ese | 4 | | Water **Che**stnuts | 5 | --- ## reportKeywordInterceptTermsPresented() This method must be called anytime a keyword suggestion(provided by the `performKeywordSearch()` method) is displayed to the user. Calling this method reports back these suggested keywords to AdAdapted. > **Note:** Making sure to call this method when a keyword is displayed will ensure AdAdapted can provide you with the most accurate reporting results. This method accepts a list of `term_id` of the displayed keywords. See `performKeywordSearch()` return type for more info on the `term_id` property. ```js aaSdk.reportKeywordInterceptTermsPresented(termIds: string[]); ``` **Return Type:** ```js void ``` --- ## reportKeywordInterceptTermSelected() This method must be called anytime a keyword suggestion(provided by the `performKeywordSearch()` method) is selected by the user from your displayed search result list. Calling this method reports back the selected keyword to AdAdapted. > **Note:** Making sure to call this method when a keyword is selected will ensure AdAdapted can provide you with the most accurate reporting results. This method accepts the `term_id` from the selected keyword. See `performKeywordSearch()` return type for more info on the `term_id` property. ```js aaSdk.reportKeywordInterceptTermSelected(termId: string); ``` **Return Type:** ```js void ``` --- ## List Manager This is a feature that allows you to report any items added to, crossed off, or deleted from lists from ads or any other items shown to a user by your app. The following methods allow you to interact with our List Manager feature: ### reportItemsAddedToList() This method must be called when items are added to a user's list. Calling this method reports back the provided items to AdAdapted as being added to a list. > **Note:** Making sure to call this method when items are added to a list by the user will ensure AdAdapted can provide you with the most accurate reporting results. This method accepts an item name string array. For ads displayed by this SDK, it is suggested that you send the `product_title` from the item object. For all other items, you can provide the product name string that is available to you. See `onAddToListTriggered()` return type for more info on the `DetailedListItem` object that contains the `product_title` property. This method also accepts an optional list name parameter. This will allow you to associate items to a given list for a user. If no value is provided for the list name property, those items will be associated to a default list for that user. ```js aaSdk.reportItemsAddedToList(itemNames: string[], listName?: string); ``` **Return Type:** ```js void ``` ### reportItemsCrossedOffList() This method must be called when items are crossed off a user's list. Calling this method reports back the provided items to AdAdapted as being crossed off a list. > **Note:** Making sure to call this method when items are crossed off a list by the user will ensure AdAdapted can provide you with the most accurate reporting results. This method accepts an item name string array. For ads displayed by this SDK, it is suggested that you send the `product_title` from the item object. For all other items, you can provide the product name string that is available to you. See `onAddToListTriggered()` return type for more info on the `DetailedListItem` object that contains the `product_title` property. This method also accepts an optional list name parameter. This will allow you to associate items to a given list for a user. If no value is provided for the list name property, those items will be associated to a default list for that user. ```js aaSdk.reportItemsCrossedOffList(itemNames: string[], listName?: string); ``` **Return Type:** ```js void ``` ### reportItemsDeletedFromList() This method must be called when items are deleted from a user's list. Calling this method reports back the provided items to AdAdapted as being deleted from a list. > **Note:** Making sure to call this method when items are crossed off a list by the user will ensure AdAdapted can provide you with the most accurate reporting results. This method accepts an item name string array. For ads displayed by this SDK, it is suggested that you send the `product_title` from the item object. For all other items, you can provide the product name string that is available to you. See `onAddToListTriggered()` return type for more info on the `DetailedListItem` object that contains the `product_title` property. This method also accepts an optional list name parameter. This will allow you to associate items to a given list for a user. If no value is provided for the list name property, those items will be associated to a default list for that user. ```js aaSdk.reportItemsDeletedFromList(itemNames: string[], listName?: string); ``` **Return Type:** ```js void ``` --- ## Out-Of-App Payload Data This is a feature that allows you to listen for items a user wants to add to their list from ads being served external to your app. The following are the two types of payloads that can occur: - **Deep Links:** If your app allows for deep links to navigate back to your app, the SDK will be able to intercept the deep links used by external AdAdapted ads and extract out the payload data from the link. This applies whether your app is in an open or closed state when the deep link is triggered. - **Payload Data API:** An API call is made to AdAdapted's Payload Data server to check and see if there are any pending payload data requests for the user. This will only be triggered when the app is initially opened or any time a user switches back to your app and its current state becomes active. A single callback is used to intercept the payload data for either of the two approaches above. For detailed information on how to implement this method, see [Callbacks: onOutOfAppPayloadAvailable()](docs/react-native-callbacks?id=onoutofapppayloadavailable). Once you have received the payload data and processed through each provided payload, you will need to call one of the two following methods to confirm that you processed or rejected the payload: ### markPayloadContentAcknowledged() This method will allow for AdAdapted to confirm the payload was received, and the items were processed into the users list. This method accepts the `payloadId` as a parameter. You must trigger this method for each accepted payload you process. > **Note:** You must call this method if the items were processed into the users list for a particular payload. If you don't, the next time the user loads your app, the payload will still be available and retrieved again. ```js aaSdk.markPayloadContentAcknowledged(payloadId: string); ``` **Return Type:** ```js void ``` ### markPayloadContentRejected() This method will allow for AdAdapted to confirm the payload was received, and the items were rejected from being added to the users list. You can use this method if the items in the payload already exist in the users list. This method accepts the `payloadId` as a parameter. You must trigger this method for each rejected payload you process. > **Note:** You must call this method if the items were rejected from being added to the users list for a particular payload. If you don't, the next time the user loads your app, the payload will still be available and retrieved again. ```js aaSdk.markPayloadContentRejected(payloadId: string); ``` **Return Type:** ```js void ``` --- ## Ad Zones Ad zones are rendered by your app, one `AdZone` component per zone. Off-screen zones, zone visibility and recipe contexts are all handled through that component's props rather than through methods on the SDK. See [Ad Zones](docs/react-native-ad-zones.md) for the full reference. > **Note:** getAdZones(), getOffScreenAdZones(), onAdZoneVisibilityChanged(), setAdContext() and clearAdContext() were removed in v5.0.0. See the Upgrade Guide for what replaces each of them. --- ### Callbacks # Callbacks These callbacks are provided to `initialize()` and apply across every ad zone. Individual zones can also report `add to list` taps, fill state and load results through their own props — see [Ad Zones: Zone Callbacks](docs/react-native-ad-zones?id=zone-callbacks). > **Note:** onAdZonesRefreshed() was removed in v5.0.0. Each ad zone now refreshes itself, so there is no SDK-wide refresh to react to. If you were using it to collapse empty zones, use the per-zone onZoneHasAds() prop instead. ## onAddToListTriggered() This callback method is triggered when a user clicks an `add to list` ad zone. This method receives an `items` list that contains each item the user selected to add to their list. If there are any other actions you need to perform when the user adds an ad item to their list, you can do so in the SDK's initialize method. > **Note:** After the items reach the user's list, call acknowledge() for each item so the interaction is reported. ```js aaSdk.initialize({ ...other_props, onAddToListTriggered: (items) => { // See below for the type definition of the "items" list. }, }); ``` **Type Definition:** _items_ ```js [ { /** * The barcode of the product. */ product_barcode: string; /** * The brand of the product. */ product_brand: string; /** * The category of the product. */ product_category: string; /** * The discount given for the product. */ product_discount: string; /** * The image used for display of the product. */ product_image: string; /** * The SKU of the product. */ product_sku: string; /** * The name/title of the product. */ product_title: string; } ] ``` --- ## onOutOfAppPayloadAvailable() This callback method is triggered when an `out-of-app payload` is available for processing. This allows you to process all available payloads, each including a list of products a user would like to add to their list. If there are any other actions you need to perform when `out-of-app payload` data becomes available to process, you can do so in the SDK's initialize method. ```js aaSdk.initialize({ ...other_props, onOutOfAppPayloadAvailable: (payloads) => { // See below for the type definition of the "payloads" list. }, }); ``` **Type Definition:** _payloads_ ```js [ { /** * The payload ID associated to the provided list items. */ payload_id: string; /** * The list of products to be added to a list. */ detailed_list_items: [ { /** * The barcode of the product. */ product_barcode: string; /** * The brand of the product. */ product_brand: string; /** * The category of the product. */ product_category: string; /** * The discount given for the product. */ product_discount: string; /** * The image used for display of the product. */ product_image: string; /** * The SKU of the product. */ product_sku: string; /** * The name/title of the product. */ product_title: string; } ] } ] ``` > **Note:** When processing the payloads, make sure to finish by calling either markPayloadContentAcknowledged() or markPayloadContentRejected() after each payload is processed. --- ### Upgrade Guide # Upgrade Guide **Quick Links:** - [Upgrading to v5.0.0](#upgrading-to-v500) --- # Upgrading to v5.0.0 **v5.0.0** moves the React Native SDK onto the v1.0.0 ad service. Ads are served **one per zone** rather than a list for every zone, sessions are generated on the client rather than minted by an API call, and **your app renders the ad zones** rather than reading them back from the SDK. > **Note:** v5.0.0 is a breaking release. Any implementation on 4.x requires the migration described on this page. The largest change is that getAdZones() is gone — there is no zone list to read, because the service no longer has a route that enumerates zones. Your app declares the zones it was allocated instead. To upgrade: ```bash npm install --save @adadapted/react-native-sdk@^5.0.0 ``` > **Note:** Re-run pod install from your ios folder after upgrading. ## Rendering Ad Zones Previously the SDK built the zone components and you found the one you wanted in the list it handed back: ```jsx { adZoneInfoList && adZoneInfoList.find((zone) => zone.zoneId === "123456").adZone; } ``` Now you render an `AdZone` for each zone AdAdapted allocated to you, passing the zone ID directly: ```jsx import { AdZone } from "@adadapted/react-native-sdk"; ; ``` The zone IDs come from whoever configured your placements with AdAdapted. See [Ad Zones](docs/react-native-ad-zones.md) for the full component reference. > **Note:** There is no longer any zone list to hold in component state, so the state property most implementations kept for adZoneInfoList (and the onAdZonesRefreshed handler that updated it) can be deleted outright. --- ## isVisible is Required As of **v5.0.0**, `AdZone` requires the `isVisible` prop. Pass `true` for a zone that is always on screen, or report actual visibility for a zone that can be scrolled away or left on an unfocused screen. It is required rather than defaulted because both defaults fail silently, and React Native gives the SDK no way to detect visibility itself: - Defaulting to `true` over-reports impressions for a zone left mounted on a screen the user navigated away from. - Defaulting to `false` is worse. The ad is still fetched and still rendered to the user, but no impression is ever reported and the zone never refreshes, so a zone whose prop you overlooked would serve ads and report nothing indefinitely, with nothing raised. Requiring it turns either silent failure into a compile error. See [Ad Zones: Reporting Visibility](docs/react-native-ad-zones?id=reporting-visibility) for how to source the value. --- ## Removed | Removed | Replacement | | ------------------------------ | ----------------------------------------------------------------------------------------- | | `getAdZones()` | Render `` for each zone. | | `getOffScreenAdZones()` | The same `AdZone` component, with `isVisible` reporting whether it is scrolled into view. | | `AdZoneInfo` | None. There is no zone list. | | `onAdZonesRefreshed` | None. Each zone refreshes itself; use `onZoneHasAds` if you need to react to fill state. | | `onAdZoneVisibilityChanged()` | The per-zone `isVisible` prop. | | `setAdContext()` | The per-zone `contextId` prop. | | `clearAdContext()` | The per-zone `contextId` prop, set to `undefined`. | | `offScreenAdZoneIds` prop | None. Off-screen zones are ordinary zones that report their own visibility. | | `storeCurrentSessionId` bridge | None. This native method was internal. | --- ## Added - **`AdZone`** — the ad zone component your app renders. See [Ad Zones](docs/react-native-ad-zones.md). - **`storeId`** on `initialize()` — the store to target ads for, if you are targeting ads by store. - **Type exports.** `EnvironmentTypes`, `DeviceTypes`, `AdZoneTypes`, `DetailedListItem` and `OutOfAppDataPayload` are now exported from the package entry point. Previously you had to reach into `src/` to name the types of the API you were calling. --- ## Type Checking `AdZone`'s props type was previously emitted with an unresolvable import path, so under `skipLibCheck` — which almost every React Native app enables — the props degraded to `any` and **no prop was type checked at all**, including `zoneId`. > **Note:** That is fixed, so the declaration now resolves. If you were relying on the props being effectively unchecked, you may see new and real type errors on your ad zone usage. They were always there; they simply could not be reported before. --- ## Behavior Changes These do not change any signature. Existing code keeps compiling and then behaves differently. ### One ad per zone Each zone requests its own ad and refreshes on its own schedule, using the refresh time served with that ad. There is no client side cycling through a list of ads, and no SDK-wide refresh. The countdown is paused while the zone reports `isVisible={false}` and while the app is backgrounded. ### Sessions are generated on the client Sessions are generated in JS, held in memory and **never persisted**. A relaunch therefore always starts a new session, and only returning to the foreground within **30 minutes** resumes the existing one. There is no session API call, and no session object to read back — `getSessionId()` returns the ID as soon as `initialize()` resolves. > **Note:** Session IDs are now RN followed by 32 characters. Reporting resolves the platform from that prefix. ### Interactions are reported on a real tap An interaction used to be reported on touch start, so any scroll that began on an ad counted as a click, and `acknowledge()` then reported a second one for the same ad. An interaction is now reported once, on an actual tap. Use `xyDragDistanceAllowed` to tune how much drag still counts as a tap. ### Ad zones no longer interfere with each other Two defects made multiple zones unsafe on the same screen, and both are fixed by the move to per-zone components: - The ad refresh timer was module scoped, so every zone shared one timer and multiple zones clobbered each other. - A zone's cleanup tore down every other zone's event listeners. ### iOS device identifier On iOS the SDK previously stored the session ID under the same key that the advertising identifier reads as its fallback, so a device with tracking denied reported a different device ID every session. It now uses the vendor identifier, which is stable and needs no tracking prompt. --- ### Caveats # Caveats ## Potential Issue 1 > **Note:** This error is only related to Android. The first potential issue you can have when setting up the SDK is the following error: ```text Android Studio Could not initialize class org.codehaus.groovy.runtime.InvokerHelper ``` If this occurs, you can fix it by updating your gradle version in the `gradle-wrapper.properties` file as suggested [in this post](https://stackoverflow.com/questions/35000729/android-studio-could-not-initialize-class-org-codehaus-groovy-runtime-invokerhel). --- ## Potential Issue 2 The second potential issue is related to one of the NPM packages required by the SDK. ```text Invariant Violation: requireNativeComponent: "RNCWebView" was not found in the UIManager ``` If this occurs, you can fix it by directly installing the `react-native-webview` package if your project doesn't already use it. ```bash npm install --save react-native-webview ``` > **Note:** Make sure to re-run `pod install` from your ios folder once the `react-native-webview` package finishes installing. --- ## In-App Popup Ad Types Are Not Displayed The SDK displays two of the four ad action types: | Ad type | Displayed | | --- | --- | | Add to list | Yes | | Standard external link (opens in external browser) | Yes | | Add to list circular (in-app popup) | No | | Popup external link (in-app popup) | No | An ad served with an action type the SDK does not handle stays on screen and does nothing when tapped, rather than costing the zone its ad. Nothing is reported for the tap. > **Note:** This is not new in v5.0.0; 4.x handles the same two action types. The Android SDK does display the popup types, so the same campaign can behave differently on React Native than it does on native. If your campaigns rely on them, talk to AdAdapted. --- ## Reporting Ad Zone Visibility React Native gives the SDK no way to detect whether an ad zone is actually in front of the user, so your app has to report it through the required `isVisible` prop. The case that catches most implementations is a native navigation stack: it keeps a screen mounted when another screen is pushed on top of it. Nothing about the ad zone component changes, so unless you report it, the zone keeps counting down and recording impressions for an ad nobody can see. Combine the screen's focus state with scroll visibility for zones inside a scroll view. See [Ad Zones: Reporting Visibility](docs/react-native-ad-zones?id=reporting-visibility) for the patterns. --- ## Sessions Do Not Survive a Relaunch As of **v5.0.0**, sessions are generated on the client, held in memory and never persisted. A relaunch always starts a new session; only returning to the foreground within 30 minutes resumes the existing one. > **Note:** This is expected behavior rather than a defect, but it is worth knowing when reading reports or debugging: a session ID you saw before restarting the app will not come back. --- ## Limitations - This SDK uses timers to run each ad zone's refresh countdown. You will most likely see the following warning message displayed in the debug console when running in an emulator: ```text Setting a timer for a long period of time, i.e. multiple minutes, is a performance and correctness issue on Android as it keeps the timer module awake, and timers can only be called when the app is in the foreground. See https://github.com/facebook/react-native/issues/12981 for more info. (Saw setTimeout with duration 300000ms) ``` --- ## Javascript SDK ### Getting Started # Getting Started > **Note:** v3.0.0 moves the SDK onto single ad serving and client generated sessions. It is a breaking change, and any implementation on 2.x requires a migration — start with the Upgrade Guide. ## Features - Ad zone creation, one ad per zone, each refreshing on its own schedule - In-app ad display popup - In-list product adding suggestion based on available keywords - Recipe contextual ads - Out-of-app payload data - Event tracking to enable reporting - Supported ad types: - Add to list (reports a product to add to a list) - Popup external link (opens in-app within popup view) - Add to list circular (opens in-app within a popup view and displays a multi-item "add to list" circular) ## Installation ```bash npm install --save @adadapted/js-sdk ``` The latest release is **v3.0.0**. To pin to it explicitly: ```bash npm install --save @adadapted/js-sdk@^3.0.0 ``` ## License [AdAdapted Platform License](docs/license.md) --- ### Usage # Usage > **Note:** This page describes v3.0.0, which serves one ad per zone per request and generates sessions on the client. If you are coming from 2.x, read the Upgrade Guide first — v3.0.0 is a breaking change, and some of it keeps compiling while behaving differently. Start by importing the `AdadaptedJsSdk` class. You should do this in your web app's main entry file, so you are defining the SDK reference once and passing it around as needed. ```js import AdadaptedJsSdk from "@adadapted/js-sdk/src"; ``` --- Now that the SDK has been imported, you need to create a reference to the SDK and `initialize` it. In the below example, you will need to replace the string `API_KEY` with the API key provided to you by AdAdapted and the string `USEWR_UDID` with the ID of the user. The `initialize()` method is promise based, so once the `.then()` callback occurs, the SDK is initialized and you can start interfacing as needed with your reference to the SDK. The `zonePlacements` object is required to allow ads to load to your specified zone elements. AdAdapted will provide you with zone IDs for all zones your app supports. The `zonePlacements` object is to provide the SDK a mapping of the AdAdapted provided zone IDs to the IDs of the HTML element you intend to contain that zone within. You should provide a definition for the `onAddItemsTriggered()` and `onPayloadsAvailable()` methods if you are using the `add to list` feature of the SDK. See [Callbacks: onAddItemsTriggered()](docs/javascript-callbacks?id=onadditemstriggered) and [Callbacks: onPayloadsAvailable()](docs/javascript-callbacks?id=onpayloadsavailable) for more details. Each zone requests its own ad and refreshes on its own schedule. Use [Callbacks: onAdRetrieved()](docs/javascript-callbacks?id=onadretrieved) if you need to know whether a given zone was filled — for example to collapse its container element. It is told one zone at a time, as each zone's request resolves. ```js const aaSdk = new AdadaptedJsSdk(); aaSdk .initialize({ apiKey: "API_KEY", advertiserId: "USER_UDID", allowRetargeting: true, zonePlacements: { 1234: "zone-id-1", 5678: "zone-id-2", }, params: { // NOTE: The params object is optional. Only use it if you have the need to. storeId: "SOME_STORE_ID", // Only used if targeting by store ID. recipeContextId: "1111", // Only used if using contextual ads for recipes. recipeContextZoneIds: ["1234", "5678"], // Only used if using contextual ads for recipes. }, onAddItemsTriggered: (items, adContent) => { // Triggered when the add-to-list or add-to-cart action has occurred. // Once the items have reached the user's list, acknowledge through // the handle to report the ad's interaction. addItemsToUsersList(items).then(() => { adContent.acknowledge(); }); }, onPayloadsAvailable: (payloads) => { // Triggered when payloads are available for processing. }, onAdRetrieved: (zoneId, hasAd) => { // Triggered as each zone's ad request resolves. }, }) .then(() => { // SDK has been initialized. console.log("SDK Session ID:", aaSdk.getSessionId()); }) .catch((err) => { console.error(err); }); ``` > **Note:** The above code defines a variable named aaSdk. This is the reference to the SDK that you should pass around your app as needed. > **Note:** The advertiserId you provide is expected to be stable for a given user. Sessions are generated on the client and cached in localStorage keyed partly on this ID, so a new ID on every page load means every visit becomes its own session. See the Upgrade Guide for details. > **Note:** The above code defines a property named params. This property should only be used when you are using a feature that requires one of the child properties of the params object. If you are not using a feature that requires params, this property should be omitted from your initialization of the SDK. --- When you are finished with the SDK, call `unmount()` to release its per-zone refresh timers, the `IntersectionObserver` it uses to watch zone visibility, and its document event listeners. In a single page app this belongs in the teardown of whatever owns the SDK reference: ```js aaSdk.unmount(); ``` --- > **Note:** The following only applies if your ad zones scroll within an element rather than the page itself. If your zones live inside a scrolling container, pass that container's element ID as `scrollContainerId` when you initialize. The SDK measures zone visibility against that element's box instead of the browser viewport, which is what decides when a zone counts as being in front of the user. That element must be an **ancestor of every placement element**. A zone outside it never counts as visible, so it records no impressions and never refreshes. If your zones are spread across more than one scrolling region, leave `scrollContainerId` unset and let visibility be measured against the viewport. --- > **Note:** The following only applies if you are using the Keyword Intercept feature. When a user is using your product search feature, you can request for suggested ad items from the AdAdapted SDK based on their search term/value. You can then display the suggested ad items along with the search results within the displayed list of products. To do this, you can call the `.performKeywordSearch()` method on your SDK reference and pass it the search term/value as follows: ```js const aasdkSearchResults = aaSdk.performKeywordSearch(searchValue); ``` The result is a list of ad items that can be displayed. This list is already pre-sorted in the order of usage priority. Based on the number of ad items you would like to display in your resulting list, you can grab that many items from the list starting from index zero. When using Keyword Intercepts, you must report when an item is _presented_ to and _selected_ by a user. Your SDK reference has two methods you will need to call when these scenarios occur. Call the following when the user is _presented_ a Keyword Intercept ad item that was provided by the AdAdapted SDK: ```js aaSdk.reportKeywordInterceptTermsPresented([aasdkSearchResults[0].term_id, aasdkSearchResults[1].term_id]); ``` This will make an API call to AdAdapted to report the ad item(s) that you decided to displayed to the user. The method accepts an array of `term_id` and must be provided to the method as an array even if you only displayed a single ad item to the user. Making sure to call this method when an ad item is presented will enable accurate reporting in the reports AdAdapted provides you. Call the following when the user has _selected_ a Keyword Intercept ad item from the search result list you present to them: ```js aaSdk.reportKeywordInterceptTermSelected(selectedItem.item.term_id); ``` > **Note:** The above code assumes you have the ad item object selectedItem available to you to extract and send the term_id at the time the user selected the item from your list. --- ### Methods # Methods ## initialize() The initialize method is where your SDK session is created. This method returns a promise that when complete, your reference to the SDK will have a valid SDK session going forward. Once you have a valid session, you will be able to successfully use other SDK methods. You should call this method as soon as possible and only once (upon page load / in your app constructor). ```js aaSdk.initialize({ /** * The API key provided to you by AdAdapted. * If you have multiple variations of the app, you will be provided with multiple API keys. * For each API key you have, you will need to determine the correct one to provide the SDK when initializing based on the users variation of the app. * Must be provided for the SDK to initialize. */ apiKey: string, /** * The unique ID to use during initialization. * This doesn't have to be the advertiser ID. You can provide any ID that you would like to uniquely identify a user. */ advertiserId: string, /** * The flag that allows the sdk to track the device for ad retargeting. * If a user 'opts out' of any privacy concerns (i.e. iOS 14 privacy update) for tracking them outside the app, this must be set to false, otherwise can be true to improve ad targeting and ad experience. */ allowRetargeting: bool, /** * (Optional) * Enables the ability to retrieve out-of-app payloads for the user. * See the "Out-Of-App Payload Data" section below. */ enablePayloads: bool, /** * (Optional) * Enables the ability to retrieve keyword intercepts. * Must be true for performKeywordSearch() to return results. */ enableKeywordIntercept: bool, /** * (Optional) * The bundle ID used by your own app. * This helps us to better identify the proper reporting data on ad related events. */ bundleId: string, /** * (Optional) * The bundle version used by your own app. * This helps us to better identify the proper reporting data on ad related events. */ bundleVersion: string, /** * The zone placement mapping object. * Ad zones will not be rendered on screen unless this mapping is provided. * key: The zone ID provided to you by AdAdapted. * value: The element ID of the container element you want the zone rendered in. */ zonePlacements: {[key: string]: string}, /** * (Optional) * Provide this value to define what API environment the SDK should use. * Production is the default value if you don't provide this property in your config. */ apiEnv: "prod" | "dev", /** * (Optional) * Provide this value if you need to control the locale of the ads displayed. * Example: en-US, en-CA, fr-CA */ deviceLocale: string, /** * (Optional) * The ID of the element the ad zones scroll within, used to decide when a * zone is actually in front of the user. This element becomes the root that * zone visibility is measured against, so it must be an ancestor of every * placement element - a zone outside it is never reported as visible at all, * so it will never record an impression or refresh. * If this is not provided, zones are measured against the browser viewport. */ scrollContainerId: string, /** * (Optional) * Provide this object if you have a need to use any of it's properties. */ params: { /** * (Optional) * If provided, the ads served will be targeted by this store ID. */ storeId: string, /** * (Optional) * If provided, the ads served to the specified zone IDs will be based on the provided recipe context ID. */ recipeContextId: string, /** * (Optional) * If provided, the ads served to the specified zone IDs will be based on the provided recipe context ID. */ recipeContextZoneIds: string[], }, /** * (Optional) * Triggered when a zone swaps the ad it is showing for a new one. * Not called for a zone's first ad, only for the ones that replace it. */ onAdZoneRefreshed: (zoneId) => {}, /** * (Optional) * Triggered when "add to list" or "add to cart" * actions occur from a user interacting with an ad zone. * Call adContent.acknowledge() once the items have reached the * user's list, which is what reports the ad's interaction. */ onAddItemsTriggered: (items, adContent) => {}, /** * (Optional) * Triggered when a user clicks an ad that represents external(non-app) content. */ onExternalContentAdClicked: (adId) => {}, /** * (Optional) * Triggered when user's "add to list" / "add to cart" payloads have been retrieved. */ onPayloadsAvailable: (payloads) => {}, /** * (Optional) * Triggered when a zone's ad request resolves, told one zone at a * time as each zone answers on its own schedule. */ onAdRetrieved: (zoneId, hasAd) => {}, }) ``` **Return Type:** ```js Promise ``` > **Note:** As of v3.0.0, sessions are generated by the SDK on the client and cached in localStorage, so a session survives a page reload and rotates after 30 minutes of inactivity. This makes advertiserId significant beyond targeting — it is expected to be stable for a given user. See the Upgrade Guide for details. --- ## getSessionId() Once the SDK is initialized, this method returns the current session ID. > **Note:** As of v3.0.0 this returns undefined before a session exists, so check the result before using it. ```js aaSdk.getSessionId(); ``` **Return Type:** ```js string | undefined; ``` --- ## unmount() Calling this method ensures everything the SDK holds on to is released properly: the per-zone refresh timers, the `IntersectionObserver` watching zone visibility, and its document event listeners. Call it when you are finished with the SDK — for example from a component's cleanup function — to avoid memory leaks. > **Note:** Any outstanding add to list acknowledgement handles are abandoned by this call. Acknowledging one afterwards does nothing rather than reporting against whatever replaced it. ```js aaSdk.unmount(); ``` **Return Type:** ```js void; ``` --- ## acknowledge() > **Note:** Added in v3.0.0. This method reports the interaction for an `add to list` / `add to cart` ad click, confirming that the items reached the user's list or cart. Clicking one of these ads does not report the interaction on its own — the items still have to arrive somewhere, and only your app knows whether they did. > **Note:** This method is called on the adContent handle passed to onAddItemsTriggered(), not on your SDK reference. See that callback for the full AtlAdContent type definition. The handle is bound to the click it came from, so the interaction is always reported against the right ad — even when the zone has already rotated on to its next ad, and even when more than one `add to list` click is outstanding at once. ```js onAddItemsTriggered: (items, adContent) => { addItemsToUsersList(items).then(() => { adContent.acknowledge(); }); }; ``` **Return Type:** ```js boolean ``` Returns `true` when the call reported the interaction, and `false` when there was nothing to report: - The click was already acknowledged by an earlier call. - The items came from inside an ad popover, so the interaction was already counted at the moment the popover opened. Such a handle reports `requiresAcknowledgement: false`. Check that property if you need to tell the two cases apart — if you don't, calling `acknowledge()` unconditionally is harmless. - The handle belongs to an SDK that has since been torn down by `unmount()` or re-initialized by a second `initialize()`. > **Note:** This method is safe to call more than once — only the first call reports anything — and safe to call late, including after the SDK has gone away, when it does nothing. > **Note:** Acknowledging an add to list click is required for correct performance reporting. Use this method in place of the deprecated acknowledgeAdded() below. --- ## acknowledgeAdded() > **Note:** Deprecated as of v3.0.0. Call acknowledge() on the adContent handle passed to onAddItemsTriggered() instead. This method still works and is still supported, but it cannot say which click it is confirming. This method reports the interaction for an `add to list` / `add to cart` ad click, confirming the items reached the user's list or cart. Clicking one of these ads does not report the interaction on its own, so this confirmation is required for correct performance reporting. Because this method takes no arguments, it resolves the **oldest** outstanding click. With ads served one per zone, a user can click two `add to list` ads before the first is confirmed, and items added from inside an ad popover have already had their interaction reported. In both cases this method reports against the wrong ad. The handle passed to `onAddItemsTriggered()` is bound to the click it came from and has neither problem. See the [Upgrade Guide](docs/javascript-upgrade-guide?id=acknowledgeadded) for a full explanation and a migration example. ```js aaSdk.acknowledgeAdded(); ``` **Return Type:** ```js void ``` --- ## updateStoreId() This method allows you to change the current store ID of focus. > **Note:** This method should only be used if you are using store ID targeting. ```js aaSdk.updateStoreId(newStoreId: string); ``` **Return Type:** ```js void ``` --- ## updateRecipeContextId() This method allows you to change the current recipe context ID of focus. This method only needs to be used when loading subsequent recipes within the user's current page. > **Note:** This method should only be used if you are using recipe contexts. ```js aaSdk.updateRecipeContextId(newRecipeContextId: string, newRecipContextZoneIds: string[]); ``` **Return Type:** ```js void ``` --- ## reportRecipeLoaded() > **Note:** This method has always worked, but was missing from the type definitions until v3.0.0. This method reports that a recipe has been loaded using the provided recipe context. Call it when a recipe is shown to the user, so AdAdapted can report accurately on recipe contextual ads. > **Note:** This method should only be used if you are using recipe contexts. ```js aaSdk.reportRecipeLoaded(recipeContextId: string, recipeContextZoneIds: string[]); ``` **Return Type:** ```js void ``` --- ## getAvailableKeywordIntercepts() This method can be used to verify which keyword intercept terms are available. > **Note:** This is useful during development to display all available keyword intercept terms you can test with. ```js aaSdk.getAvailableKeywordIntercepts(); ``` **Return Type:** ```js undefined | [ { /** * The search term ID. */ term_id: string; /** * The search term used to validate a search string against. */ term: string; /** * The display string that should be used to display in a list. */ replacement: string; /** * The display priority of this item. * Used to compare this to other Keyword Serach Term items to determine * the final priority order during display. The lower the number, the * higher the priority. */ priority: number; } ] ``` --- ## performKeywordSearch() Once the SDK is initialized, this method can be called when performing a search operation within your app. Call this method either as the user types in a search term or when the user executes a search after the term is entered. A list of keyword search results will be returned by this method that can then be used to display dynamically along with the search results your app displays to the user. The final result of the search may have many items, in which case you would determine how many of them you would like to display. ```js aaSdk.performKeywordSearch(searchTerm: string); ``` > **Note:** Matching happens entirely on the device. The SDK downloads the list of available keyword terms when it initializes, and performKeywordSearch checks the user's input against that local list. No network request is needed to find a match, so results come back instantly and can be displayed to the user as they type. > > For the purposes of campaign reporting, performKeywordSearch does send a lightweight background event recording the search term and whether it matched. Because search terms are reported, include Search History in your App Store privacy details and in-app search history in Google Play's Data safety form, if not doing so already. **Return Type:** ```js [ { /** * The search term ID. */ term_id: string; /** * The search term that was used to validate * the provided search string against. */ term: string; /** * The display string you can use to display the * resulting item in your search results list. */ replacement: string; /** * The display priority of this item amongst all * other keyword search results. The priority acts * as a secondary sort. The primary sort is based * on whether the "term" starts with the provided * search term characters or not. */ priority: number; } ] ``` > **Note:** Sorting based on priority is already handled for you by the SDK. The results will already be in the correct sort order. The final result of suggestions is pre-sorted based on the following criteria: - First, the suggestions are sorted based on which terms "start" with the characters in the provided search string. - Second, they are then sorted by a pre-determined priority for each keyword that satisfied the search. **Search Example:** _che_ | Keyword Term | Priority | | ------------------- | -------- | | **Che**x Mix | 2 | | **Che**ddar Cheese | 3 | | Sweet **Che**rries | 1 | | Mac & **Che**ese | 4 | | Water **Che**stnuts | 5 | --- ## reportKeywordInterceptTermsPresented() This method must be called anytime a keyword suggestion(provided by the `performKeywordSearch()` method) is displayed to the user. Calling this method reports back these suggested keywords to AdAdapted. > **Note:** Making sure to call this method when a keyword is displayed will ensure AdAdapted can provide you with the most accurate reporting results. This method accepts a list of `term_id` of the displayed keywords. See `performKeywordSearch()` return type for more info on the `term_id` property. ```js aaSdk.reportKeywordInterceptTermsPresented(termIds: string[]); ``` **Return Type:** ```js void ``` --- ## reportKeywordInterceptTermSelected() This method must be called anytime a keyword suggestion(provided by the `performKeywordSearch()` method) is selected by the user from your displayed search result list. Calling this method reports back the selected keyword to AdAdapted. > **Note:** Making sure to call this method when a keyword is selected will ensure AdAdapted can provide you with the most accurate reporting results. This method accepts the `term_id` from the selected keyword. See `performKeywordSearch()` return type for more info on the `term_id` property. ```js aaSdk.reportKeywordInterceptTermSelected(termId: string); ``` **Return Type:** ```js void ``` --- ## List Manager This feature allows you to report any items added to list/cart, deleted from list/cart, and crossed off list. This feature is not limited to items added as a result of an ad click and can be used for all items in a user's list/cart. The following methods allow you to interact with our List Manager feature: ### reportItemsAddedToList() This method must be called when items are added to a user's list. Calling this method reports back the provided items to AdAdapted as being added to a list. > **Note:** Making sure to call this method when items are added to a list by the user will ensure AdAdapted can provide you with the most accurate reporting results. This method accepts an item name string array. For ads displayed by this SDK, it is suggested that you send the `product_title` from the item object. For all non-ad items, you can provide the product name string that is available to you. See [Callbacks: onAddItemsTriggered()](docs/javascript-callbacks?id=onadditemstriggered) return type for more info on the `DetailedListItem` object that contains the `product_title` property. This method also accepts an optional list name parameter. This will allow you to associate items to a given list for a user. If no value is provided for the list name property, those items will be associated to a default list for that user. ```js aaSdk.reportItemsAddedToList(itemNames: string[], listName?: string); ``` **Return Type:** ```js void ``` --- ### reportItemsCrossedOffList() This method must be called when items are crossed off a user's list. Calling this method reports back the provided items to AdAdapted as being crossed off a list. > **Note:** Making sure to call this method when items are crossed off a list by the user will ensure AdAdapted can provide you with the most accurate reporting results. This method accepts an item name string array. For ads displayed by this SDK, it is suggested that you send the `product_title` from the item object. For all non-ad items, you can provide the product name string that is available to you. See [Callbacks: onAddItemsTriggered()](docs/javascript-callbacks?id=onadditemstriggered) return type for more info on the `DetailedListItem` object that contains the `product_title` property. This method also accepts an optional list name parameter. This will allow you to associate items to a given list for a user. If no value is provided for the list name property, those items will be associated to a default list for that user. ```js aaSdk.reportItemsCrossedOffList(itemNames: string[], listName?: string); ``` **Return Type:** ```js void ``` --- ### reportItemsDeletedFromList() This method must be called when items are deleted from a user's list. Calling this method reports back the provided items to AdAdapted as being deleted from a list. > **Note:** Making sure to call this method when items are deleted from a list by the user will ensure AdAdapted can provide you with the most accurate reporting results. This method accepts an item name string array. For ads displayed by this SDK, it is suggested that you send the `product_title` from the item object. For all non-ad items, you can provide the product name string that is available to you. See [Callbacks: onAddItemsTriggered()](docs/javascript-callbacks?id=onadditemstriggered) return type for more info on the `DetailedListItem` object that contains the `product_title` property. This method also accepts an optional list name parameter. This will allow you to associate items to a given list for a user. If no value is provided for the list name property, those items will be associated to a default list for that user. ```js aaSdk.reportItemsDeletedFromList(itemNames: string[], listName?: string); ``` **Return Type:** ```js void ``` --- ### reportItemsAddedToCart() This method must be called when items are added to a user's cart. Calling this method reports back the provided items to AdAdapted as being added to a cart. > **Note:** Making sure to call this method when items are added to a cart by the user will ensure AdAdapted can provide you with the most accurate reporting results. This method accepts an item name string array. For ads displayed by this SDK, it is suggested that you send the `product_title` from the item object. For all non-ad items, you can provide the product name string that is available to you. See [Callbacks: onAddItemsTriggered()](docs/javascript-callbacks?id=onadditemstriggered) return type for more info on the `DetailedListItem` object that contains the `product_title` property. This method also requires the cart ID to be provided so thae items are associated to the correct cart. ```js aaSdk.reportItemsAddedToCart(itemNames: string[], cartId: string); ``` **Return Type:** ```js void ``` --- ### reportItemsDeletedFromCart() This method must be called when items are deleted from a user's cart. Calling this method reports back the provided items to AdAdapted as being deleted from a cart. > **Note:** Making sure to call this method when items are deleted from a cart by the user will ensure AdAdapted can provide you with the most accurate reporting results. This method accepts an item name string array. For ads displayed by this SDK, it is suggested that you send the `product_title` from the item object. For all non-ad items, you can provide the product name string that is available to you. See [Callbacks: onAddItemsTriggered()](docs/javascript-callbacks?id=onadditemstriggered) return type for more info on the `DetailedListItem` object that contains the `product_title` property. This method also requires the cart ID to be provided so thae items are associated to the correct cart. ```js aaSdk.reportItemsDeletedFromCart(itemNames: string[], cartId: string); ``` **Return Type:** ```js void ``` --- ## Out-Of-App Payload Data This is a feature that allows you to retrieve items a user wants to add to their list/cart from ads being served external to your app. An API call is made to AdAdapted's Payload Data server to check and see if there are any pending payload data requests for the user. This will only be triggered when the SDK is initialize or any time a user switches back to your app and its current state becomes active. The `onPayloadsAvailable()` callback is triggered once data is extracted/ready for you to process. See [Callbacks: onPayloadsAvailable()](docs/javascript-callbacks?id=onpayloadsavailable) for implementation information. Once you have received the payload data and processed through each provided payload, you will need to call the following method to confirm that you processed or rejected the payload: ### updatePayloadStatus() This method will report back to AdAdapted confirming the payload was either delivered/accepted or rejected. This method accepts a list of `PayloadStatus` objects to allow for you to batch process multiple payloads at once. > **Note:** You must trigger this method once you have processed available payloads for a user. If you don't, the next time the user loads your app, the payload will still be available and retrieved again. ```js aaSdk.updatePayloadStatus(payloadStatusList: PayloadStatus[]); ``` **Return Type:** ```js void ``` **Type Definition:** _PayloadStatus_ ```js { /** * The payload ID. */ payload_id: string; /** * The status being assigned to the payload. */ status: "delivered" | "rejected"; } ``` --- ### Callbacks # Callbacks ## onAdZoneRefreshed() This callback method is useful for knowing when a zone swaps the ad it is showing for a new one. Each zone refreshes on its own schedule, based on the `refresh_time` of the ad it is currently displaying. > **Note:** This is purely a method for knowing when a zone's ad changed so you may perform any actions you need when this occurs. The SDK handles updating the ad zone without you needing to do anything. > **Note:** This callback is not called for a zone's first ad, only for the ones that replace it. ```js aaSdk.initialize({ ...other_props, onAdZoneRefreshed: (zoneId) => { // The ID of the ad zone whose ad changed. }, }); ``` > **Note:** Renamed in v3.0.0. Through 2.x this was onAdZonesRefreshed() and took no arguments. See the Upgrade Guide for details. --- ## onAddItemsTriggered() This callback method is triggered when a user clicks an `add to list` / `add to cart` ad. This method provides an `items` list that contains each item the user selected to add, and, as of **v3.0.0**, an `adContent` handle representing the click those items came from. Clicking an `add to list` ad is not on its own the reported interaction — the items still have to reach the user's list, and only your app knows whether they did. Once the items have been added, call `adContent.acknowledge()` to report the ad's interaction. If there are any other actions you need to perform when the user adds an ad item to their list/cart, you can do so at this time. ```js aaSdk.initialize({ ...other_props, onAddItemsTriggered: (items, adContent) => { // See below for the type definitions of "items" and "adContent". addItemsToUsersList(items).then(() => { adContent.acknowledge(); }); }, }); ``` > **Note:** Acknowledging through adContent is required for correct performance reporting. Use it in place of the deprecated acknowledgeAdded() method, which cannot say which click it is confirming. See Methods: acknowledge(). See the Upgrade Guide for details. **Type Definition:** _items_ ```js [ { /** * The barcode of the product. */ product_barcode: string; /** * The brand of the product. */ product_brand: string; /** * The category of the product. */ product_category: string; /** * The discount given for the product. */ product_discount: string; /** * The image used for display of the product. */ product_image: string; /** * The SKU of the product. */ product_sku: string; /** * The name/title of the product. */ product_title: string; } ] ``` **Type Definition:** _adContent_ (`AtlAdContent`) ```js { /** * The ad the items came from. */ adId: string; /** * The zone that ad was served into. */ zoneId: string; /** * Whether acknowledging this click will report anything. * * False for items added from inside an ad popover: opening the popover * was itself the interaction and it was reported then, so there is * nothing left to confirm. Acknowledging one of those is harmless, it * simply does nothing. Check this if you need to tell the two apart. */ requiresAcknowledgement: boolean; /** * Reports the ad's interaction, confirming the items reached the list. * * Safe to call more than once, and safe to call late - including after * the SDK has been unmounted or re-initialized, when it does nothing. * * Returns true if this call reported the interaction, false if there * was nothing to report - already acknowledged, already counted at the * moment the popover opened, or belonging to an SDK that has since * gone away. */ acknowledge(): boolean; } ``` > **Note:** The handle is bound to the click it came from, so it stays correct even when the zone has already rotated on to its next ad by the time your app is done, and when more than one add to list click is outstanding at once. --- ## onExternalContentAdClicked() This callback method is triggered when a user clicks an ad that represents external (non-app) content. This method provides an Ad ID that represents the ad that was clicked. If there are any other actions you need to perform when the user clicks an external content ad, you can do so at this time. ```js aaSdk.initialize({ ...other_props, onExternalContentAdClicked: (adId) => { // The ID of the ad that was clicked. }, }); ``` --- ## onPayloadsAvailable() This callback method is triggered when payload data is available for processing. If there are any other actions you need to perform when payload data becomes available, you can do so at this time. ```js aaSdk.initialize({ ...other_props, onPayloadsAvailable: (payloads) => { // See below for the type definition of the "payloads" array. }, }); ``` **Type Definition:** _payloads_ ```js [ { /** * The payload ID associated to the provided list items. */ payload_id: string; /** * The list of products to be added to a list. */ detailed_list_items: [ { /** * The barcode of the product. */ product_barcode: string; /** * The brand of the product. */ product_brand: string; /** * The category of the product. */ product_category: string; /** * The discount given for the product. */ product_discount: string; /** * The image used for display of the product. */ product_image: string; /** * The SKU of the product. */ product_sku: string; /** * The name/title of the product. */ product_title: string; } ] } ] ``` > **Note:** When processing payloads, make sure to finish by calling updatePayloadStatus() with a status of delivered or rejected for each payload you processed. See Methods: updatePayloadStatus(). > **Note:** Items that arrive through this callback did not come from an ad click, so there is no interaction to acknowledge for them. --- ## onAdRetrieved() This callback method is triggered when a zone's ad request resolves. Ads are requested one per zone, so this is told **one zone at a time**, as each zone answers on its own schedule. It provides the zone ID and a boolean indicating whether an ad is available for that zone. This is useful for hiding or collapsing a zone's container element when no ad was served for it. ```js aaSdk.initialize({ ...other_props, onAdRetrieved: (zoneId, hasAd) => { // The ID of the ad zone the result is for, and whether an ad is // available for that zone. }, }); ``` > **Note:** Renamed in v3.0.0. Through 2.x this was onAdsRetrieved(), and it handed over a map of every zone at once. A zone that has not answered yet is not a zone without an ad, so that map can no longer be produced — act on each zone as it is reported instead. See the Upgrade Guide for details. --- ### Upgrade Guide # Upgrade Guide **Quick Links:** - [Upgrading to v3.0.0](#upgrading-to-v300) --- # Upgrading to v3.0.0 **v3.0.0** moves the Javascript SDK onto the v1.0.0 ad service. Ads are now requested **one per zone per request** rather than arriving in bulk, and sessions are generated on the client rather than issued by the server. > **Note:** v3.0.0 is a breaking release. Any implementation on 2.x requires the migration described on this page. Everything below is either a change you have to make or a change in behavior you need to know about. > **Note:** Read the Behavior Changes With No Signature Change section even if your code still compiles — several changes keep working and then behave differently. To upgrade: ```bash npm install --save @adadapted/js-sdk@^3.0.0 ``` ## Deprecated ### acknowledgeAdded() `acknowledgeAdded()` still works and is still supported, but it cannot say **which** click it is confirming. Clicking an `add to list` ad does not report the interaction on its own: the items still have to reach the user's list, and only your app knows whether they did. The SDK therefore waits for you to confirm. With ads served per zone, a user can click one zone's `add to list` ad and then another's before your app has finished with the first, so there can be more than one confirmation outstanding. `acknowledgeAdded()` takes no arguments, so it resolves the **oldest** outstanding click. There are two ways that goes wrong: - **Ordering.** If you confirm in the order you were called, you get the right answer. If you confirm out of order — an async list write that resolves out of order, or a confirmation UI the user dismisses out of order — each interaction is reported against the wrong ad. - **The ad popover.** Items added from inside a popover also arrive through `onAddItemsTriggered()`, but that click's interaction was already reported when the popover opened. Calling `acknowledgeAdded()` for those items resolves some _unrelated_ outstanding click instead — reporting an interaction the user never confirmed, while the popover's own add confirms nothing. **Use the `adContent` handle passed to `onAddItemsTriggered()` instead.** It is bound to the click it came from and cannot be misattributed: ```js onAddItemsTriggered: (items, adContent) => { addItemsToUsersList(items).then(() => { adContent.acknowledge(); }); }; ``` The equivalent before, which is what to migrate away from: ```js onAddItemsTriggered: (items) => { addItemsToUsersList(items).then(() => { aaSdk.acknowledgeAdded(); }); }; ``` `acknowledge()` returns `true` when it reported the interaction and `false` when there was nothing to report. It is safe to call more than once — only the first call reports anything — and safe to call late. The handle has neither of the problems above. It is bound to the click it came from, and the one handed over for a popover add is inert because that interaction is already counted. That case is visible rather than silent: such a handle reports `requiresAcknowledgement: false`, and acknowledging it returns `false`. If you don't need to tell the two apart, you can call `acknowledge()` unconditionally. > **Note:** A handle is abandoned when the SDK is torn down by unmount() or re-initialized by a second initialize(). Acknowledging one after that does nothing rather than reporting against whatever replaced it. See [Methods: acknowledge()](docs/javascript-methods?id=acknowledge) for the full behavior of the handle, and [Callbacks: onAddItemsTriggered()](docs/javascript-callbacks?id=onadditemstriggered) for the full `AtlAdContent` type definition. --- ## Renamed | Before | After | | ------------------------------ | ----------------------------------------- | | `onAdsRetrieved(map)` | `onAdRetrieved(zoneId, hasAd)` | | `onAdZonesRefreshed()` | `onAdZoneRefreshed(zoneId)` | | `onAddItemsTriggered(items)` | `onAddItemsTriggered(items, adContent)` | | `Ad.ad_id` | `Ad.id` | `onAddItemsTriggered()` gains a second argument, the `adContent` handle described under [Deprecated](#deprecated). Existing handlers keep working — they simply ignore it — but confirming through the handle is the only way to get the attribution right. `onAdRetrieved()` reports **one zone at a time**, as each zone's request resolves. The old callback handed over a map of every zone at once. That map cannot be produced honestly any more: zones answer on their own schedule, so building it would mean guessing at the zones that had not answered yet, and a zone still waiting is not a zone without an ad. If you used the map to hide empty containers, act on each zone as it is reported instead. --- ## Removed | Removed | Replacement | | ------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | | `AdZoneInfo` | Zones are placed by `zonePlacements`; the SDK renders into your elements. | | `AdPopup`, `Ad.popup` | No longer served. | | `Ad.type`, `Ad.tracking_html`, `Ad.hide_after_interaction` | No longer served. | | `Zone.id`, `Zone.ads[]`, `Zone.land_width`, `Zone.land_height` | `Zone.ad` — a zone carries a single ad. | | `sessionInfo` | `getSessionId()`. There is no server session object any more. | | `adZones` | None. The SDK owns zone state. | | `lastSelectedATL` | The `adContent` handle passed to `onAddItemsTriggered()`. | | `cycleAdTimers`, `refreshAdZonesTimer`, `refreshSessionTimer`, `scrollEventAbortController`, `adZoneCurrentAdImpressionTracker` | None. Internal. | Internal state is no longer declared at all: `zones`, `adZoneAdAvailabilityMap`, `intersectionObserver`, `documentEventAbortController`, `hashedApiKey`, `apiEnvString`, `sessionCreatedAt`, `sessionLastActiveAt`, `sessionPersistedAt` and `sessionIsBackgrounded`. > **Note:** None of the above was usable API. It can change at any time and will not be treated as a breaking change. --- ## Added - **`AtlAdContent`** — the acknowledgement handle described under [Deprecated](#deprecated). - **`Ad.zone_id`** — the zone an ad was served into. - **`reportRecipeLoaded(recipeContextId, recipeContextZoneIds)`** is now declared. It already existed and worked; it was simply missing from the type definitions. See [Methods: reportRecipeLoaded()](docs/javascript-methods?id=reportrecipeloaded). --- ## Type Changes These are now honest about what the service actually sends. Only the `getSessionId()` row can produce a new `strictNullChecks` error in ordinary use: after this release no public method or callback hands you an `Ad`, a `Zone` or an `AdPayload`, so the other three only bite code that constructs those types by hand. | Member | Change | | ------------------------------- | ----------------------------- | | `Ad.payload` | now optional | | `Zone.ad` | now optional | | `AdPayload.detailed_list_items` | now optional | | `getSessionId()` | returns `string \| undefined` | > **Note:** A no-fill is reported either as an Ad with an empty id or as no ad at all. Treat both the same way. --- ## Behavior Changes With No Signature Change These are the ones to watch for: your existing code keeps compiling and then behaves differently. ### One ad per zone per request Each zone requests its own ad and refreshes on its own `refresh_time`. There is no client side cycling through a list of ads. ### Sessions are generated on the client Sessions are generated by the SDK and cached in `localStorage` under `aa-session-v3-{env}-{hash of api key and advertiser ID}`. The session survives a reload, slides forward while the page is the user's focus, and rotates after 30 minutes of inactivity. The two older key shapes are deleted on startup, for the current `apiEnv` only — a stale entry belonging to the other environment is left alone rather than risk clearing a session that is still in use. The advertiser ID is part of the key because a session is one person's visit to one app. Local storage is shared by everyone using the browser profile, so keying on the app alone meant that on a shared device the next person to sign in within the session window resumed the previous person's session, and the two of them reported under a single session ID. Signing back in as the same user still resumes that user's own session. > **Note:** advertiserId is expected to be stable for a given user. If you mint a new one per page load you are opting out of session continuity entirely — every visit becomes its own session. That is the correct outcome for a genuinely new user each time, but is worth knowing if the ID is being rotated by accident. Stored sessions past the 30 minute window are cleared on startup, so a rotating ID does not slowly fill the browser's storage. The key deliberately does **not** include `storeId` or the recipe context. Those travel with each ad request rather than with the session, and `updateStoreId()` and `updateRecipeContextId()` change them without starting a new session — so keying on them would split one shopper browsing several stores into several sessions. ### onAdZoneRefreshed() fires per zone, per rotation It is no longer called once per bulk refresh, which is why it was renamed to the singular and now names the zone that changed. It is still not called for a zone's first ad, only for the ones replacing it. ### scrollContainerId is now the IntersectionObserver root `scrollContainerId` is no longer a scroll listener target. It must be an **ancestor of every placement element** — a zone outside it never counts as visible, so it records no impressions and never refreshes. Visibility is measured against that element's box rather than the browser viewport. Leave it unset to measure against the viewport. ### Reports that you do not read are sent with keepalive This means they survive the page closing. The browser gives a document a single 64KB budget for keepalive bodies, shared with your own page's beacons, and rejects a request outright past it — losing the whole report. The SDK therefore drops `keepalive` from any report whose body exceeds **48KB**, deliberately under that budget. Such a report still sends; it is just no longer guaranteed to survive the page closing. ### Inside an iframe the session is tracked by page visibility alone In a top level page the SDK also watches focus, so switching to another application reports the session backgrounded even though the tab is still visible. That signal is meaningless in an iframe — `document.hasFocus()` is false until the user clicks into the frame, and the focus events fire when focus moves between the frame and the rest of the host page — so an embedded SDK ignores it. An embedded session is still reported as backgrounded when the tab itself is hidden. ### Action type `a` (app store URL) is not handled The SDK logs that it cannot action the type and leaves the ad in place. This was also true before; it is documented here because the type is part of the published `AdActionType` union. --- ### Caveats # Caveats > **Note:** This page describes v3.0.0. The Browser Requirements, Ad Zone Visibility and Embedded sections below are new in that release — see the Upgrade Guide if you are coming from 2.x. ## Browser Requirements ### Local Storage Sessions are generated by the SDK and cached in `localStorage`. If local storage is unavailable — a private browsing mode that blocks it, or a browser configured to reject storage — a session cannot be carried across page loads, so every load starts a new session. > **Note:** Local storage is shared by everyone using the browser profile, which is why the session key includes the advertiserId. Without it, on a shared device the next person to sign in within the session window would resume the previous person's session. ### Reports and Page Close Reports the SDK sends that you do not read are sent with `keepalive`, so they survive the page closing. The browser gives a document a single 64KB budget for keepalive bodies, and that budget is **shared with your own page's beacons**. A request past the budget is rejected outright, losing the whole report. The SDK therefore drops `keepalive` from any report whose body exceeds **48KB**, deliberately under that budget. Such a report still sends; it is just no longer guaranteed to survive the page closing. > **Note:** If your own page sends large keepalive bodies of its own on unload, they compete with the SDK's reports for the same 64KB. ## Ad Zone Visibility An ad zone has to be seen by the user to record an impression and to refresh. Visibility is measured with an `IntersectionObserver`, against the browser viewport by default. If you supply `scrollContainerId`, that element becomes the root the measurement is made against instead. It must be an **ancestor of every placement element** — a zone outside it never counts as visible, so it records no impressions and never refreshes. If your zones are spread across more than one scrolling region, leave `scrollContainerId` unset. ## Embedded (iframe) Implementations Inside an iframe, the session is tracked by page visibility alone. In a top level page the SDK also watches focus, so switching to another application reports the session backgrounded even though the tab is still visible. That signal is meaningless in an iframe: `document.hasFocus()` is false until the user clicks into the frame, and the focus events fire when focus moves between the frame and the rest of the host page. An embedded SDK therefore ignores focus. An embedded session is still reported as backgrounded when the tab itself is hidden. ## Cordova Implementations ### Potential Issue 1 The ad zones use an iframe to display the ad HTML. You can have issues with the content not displaying if you do not setup your app to allow iframe content to load. You must make a modification to your `config.xml` file that will allow for an iframe to load content within the app. Adding the following line will fix this problem: ```xml ``` ## Limitations - In general, iPhone has issues with scrolling an iframe. This is a known issue and will be looked at further to come up with a fix to this limitation/issue. - Currently ads with the type of `external` do not open in a browser external to the app running the ad. Each native environment has their own way of opening a browser and the "javascript" way of doing it only opens the link within the current apps view as a web page and replaces the app view. To get around this for the time being, `external` ad types will be opened within a popup within the app. - Ads with an action type of `"a"` (an app store URL) are not actioned. The SDK logs that it cannot handle the type and leaves the ad in place. This ad type is not served to web placements, but the type is part of the published `AdActionType` union. --- ## Flutter SDK ### Getting Started # Getting Started The Flutter SDK serves one ad per zone. Your app places an `AdZone` widget for each zone AdAdapted has allocated to you, and each zone owns its own ad request, its own refresh schedule and its own impression reporting. ## Features - Ad zones you place yourself, one ad per zone, each refreshing on its own schedule - Ad zones that measure their own visibility, so nothing has to be reported by hand - In-list add suggestion based on available keywords - Event tracking enabling reporting - Supported ad types: - Add to list (reports a product to add to a list) - Standard external link (opens in external browser) - Off-screen ad zones - Contextual recipe ads > **Note:** The SDK handles two of the four ad action types. An ad served with the in-app popup link (l) or app store link (a) type stays on screen and does nothing when tapped. See Caveats. ## Requirements | | Minimum | | --- | --- | | Flutter | 3.44 | | Dart | 3.12 | | Android | `minSdk` 24 | | iOS | 12.0 declared by the plugin, 15.0 in practice — Flutter's own floor | ## Installation ```bash flutter pub add adadapted_flutter_sdk ``` Or add it to your `pubspec.yaml` directly and run `flutter pub get`: ```yaml dependencies: adadapted_flutter_sdk: ^0.1.2 ``` ## Platform Setup ### iOS The SDK reads the advertising identifier through App Tracking Transparency, so your app needs a purpose string. Add `NSUserTrackingUsageDescription` to `ios/Runner/Info.plist`: ```xml NSUserTrackingUsageDescription This identifier is used to deliver and measure relevant ads. ``` #### Requesting App Tracking Transparency Your app must request the permission **before** calling [initialize()](docs/flutter-methods?id=initialize). The tracking status is read once, while device info is gathered, so a permission granted afterwards is not picked up until the next `initialize()`. The SDK does not prompt for you — your app owns when the dialog appears. Any ATT package works; the example app uses [`app_tracking_transparency`](https://pub.dev/packages/app_tracking_transparency): ```bash flutter pub add app_tracking_transparency ``` ```dart import 'dart:io'; import 'package:app_tracking_transparency/app_tracking_transparency.dart'; Future _startSdk() async { if (Platform.isIOS) { final status = await AppTrackingTransparency.trackingAuthorizationStatus; if (status == TrackingStatus.notDetermined) { await AppTrackingTransparency.requestTrackingAuthorization(); } } await _sdk.initialize(appId: 'YOUR_APP_ID'); } ``` `requestTrackingAuthorization()` is a no-op once the user has answered, so calling it on every launch is safe — the dialog is shown at most once. > **Note:** If the user denies tracking, the SDK reports an empty advertising identifier and keeps serving. It does not stop reporting. #### App Store privacy answers The SDK ships a `PrivacyInfo.xcprivacy` manifest, which Xcode folds into the privacy report it generates for your app. The report is not the App Store questionnaire — you still answer that yourself. What the SDK contributes, and what you therefore need to account for, is: | Data | Used for | Linked to identity | Used for tracking | | --- | --- | --- | --- | | Device ID (IDFA) | Third-party advertising, analytics | No | Yes | | Advertising data | Third-party advertising, analytics | No | Yes | | Product interaction | Third-party advertising, analytics | No | Yes | | Search history — only if you call [performKeywordSearch()](docs/flutter-methods?id=performkeywordsearch) | Third-party advertising, analytics | No | Yes | | Other data (device model, OS, screen, locale, timezone, carrier) | Third-party advertising, analytics | No | Yes | ### Android The plugin declares two permissions in its own manifest and both merge into your app. You do not add them yourself, but you are responsible for what they mean on your store listing. | Permission | Why | | --- | --- | | `android.permission.INTERNET` | Every ad request, creative load and event report goes over the network. The plugin declares it because Flutter's app template puts it in the **debug and profile** manifests only, so an app that makes no network calls of its own would work in every test build and serve nothing in the store build. | | `com.google.android.gms.permission.AD_ID` | Lets the Advertising ID be read on Android 13 and above. Without it Play Services returns a zeroed identifier and every event is attributed to the same non-user. | #### Play Console Data safety `AD_ID` merges into your APK without appearing in any manifest file you own, so it is easy to miss when the Data safety form asks. Google requires a declaration for advertising-ID use, and a mismatch between the declared form and the merged manifest is a common review rejection. Declare that your app collects a **Device or other ID**, is used for **Advertising or marketing** and **Analytics**, and is **not** linked to the user's identity. Your app is also subject to Google's [Advertising ID policy](https://support.google.com/googleplay/android-developer/answer/6048248). > **Note:** The SDK does not send the Advertising ID when the user has turned on Delete advertising ID or limit ad tracking. It reports an empty identifier and keeps serving. ## License [AdAdapted Platform License](docs/license.md) --- ### Usage # Usage The following is an example of how to use the SDK. Start by importing the package. A single import gives you the SDK class, the `AdZone` widget and every type they hand back. ```dart import 'package:adadapted_flutter_sdk/adadapted_flutter_sdk.dart'; ``` --- ## Creating the SDK Create one `AdadaptedFlutterSdk` and hold it for the life of your app, in the state of a widget high enough in the tree that it is not rebuilt. Pass the reference down to whatever needs it. ```dart class _DemoAppState extends State { /// The SDK instance the whole app shares. final AdadaptedFlutterSdk _sdk = AdadaptedFlutterSdk(); } ``` > **Note:** Create one instance per app. A second initialize() does not coexist with the first — it takes over every zone already placed, which then report under the second instance's session and app ID. See Caveats. --- ## Initializing Call `initialize()` once, from `initState()`. It is asynchronous, and once it resolves the SDK has a valid session and the rest of its methods are usable. Replace `YOUR_APP_ID` with the app ID provided to you by AdAdapted. You can provide a definition for `onAddToListTriggered` if you would like to use the `add to list` feature of the SDK. See [Callbacks: onAddToListTriggered](docs/flutter-callbacks?id=onaddtolisttriggered) for more details. ```dart @override void initState() { super.initState(); _initializeSdk(); } Future _initializeSdk() async { try { await _sdk.initialize( appId: 'YOUR_APP_ID', onAddToListTriggered: (items) { for (final item in items) { // Add the item to the user's list. } }, ); if (!mounted) { return; } // The session is generated locally, so it is available as soon as // initialize() resolves. setState(() { _sessionId = _sdk.sessionId; }); } on Exception catch (error) { // Handle the failure. } } ``` > **Note:** On iOS, request the App Tracking Transparency permission before this call. The tracking status is read once, while device info is gathered. See Getting Started: iOS. --- ## Cleaning Up Call `unmount()` from the `dispose()` of the widget that owns the SDK. This closes out every mounted zone, releases the HTTP client and ends the session. ```dart @override void dispose() { // Unmount the SDK, otherwise you can experience memory leaks. _sdk.unmount(); super.dispose(); } ``` --- ## Rendering Ad Zones Place an `AdZone` for each zone AdAdapted has allocated to you, wherever that zone belongs in your layout. The zone owns its own ad request, its own refresh and its own impression reporting, so there is nothing to wire up beyond placing it. ```dart SizedBox( height: 250, child: AdZone(zoneId: '123456'), ) ``` An unfilled zone is a `SizedBox.shrink()` and occupies nothing. A filled one fills whatever space its parent gives it, so it must be given bounds — either by the parent, or through the widget's own `width` and `height`. Zones measure their own visibility. Unlike the React Native SDK, there is no visibility prop you have to remember to report: a zone scrolled off screen, or covered by a pushed route, pauses its countdown and stops recording impressions on its own. > **Note:** A zone can be placed before initialize() resolves. It waits to be told the SDK is ready and requests its first ad then. Use `onZoneHasAds` to collapse or reveal the space around a zone as its fill state changes: ```dart bool _zoneHasAds = false; SizedBox( height: _zoneHasAds ? 250 : 0, child: AdZone( zoneId: '123456', onZoneHasAds: (hasAds) { setState(() { _zoneHasAds = hasAds; }); }, ), ) ``` See [Ad Zones](docs/flutter-ad-zones.md) for the full widget reference — sizing, visibility, recipe contexts, per-zone callbacks and off-screen zones. --- ## Handling Deep Links Out-of-app `add to list` payloads can arrive as deep links. Your app owns its own link routing, so the SDK does not subscribe to the platform's link stream — hand it every incoming link instead, including the one that launched the app. ```dart _sdk.handleDeepLink(url); ``` Anything that is not an AdAdapted payload link is ignored, so this is safe to call for every link your app receives. Payloads extracted from a link are delivered through [onOutOfAppPayloadAvailable](docs/flutter-callbacks?id=onoutofapppayloadavailable), the same callback the Payload server uses. --- ## Keyword Intercepts Call `performKeywordSearch()` when the user searches, report the terms you actually display, and report the one they pick. > **Note:** Each call sends the search string to AdAdapted, alongside the session and the advertising identifier. The example below calls it on every keystroke, which transmits the whole search as it is typed; calling it on submit instead sends one term per search. Pick the one your privacy posture supports before you ship. See Methods: performKeywordSearch(). ```dart void _search(String term) { final results = widget.sdk.performKeywordSearch(term); if (results.isNotEmpty) { // Only the terms actually presented to the user are reported. widget.sdk.reportKeywordInterceptTermsPresented( results.map((result) => result.termId).toList(), ); } setState(() { _results = results; }); } void _selectTerm(KeywordSearchResult result) { widget.sdk.reportKeywordInterceptTermSelected(result.termId); } ``` See [Methods: performKeywordSearch()](docs/flutter-methods?id=performkeywordsearch) for the full reference. --- ## Reporting List Activity When an `add to list` ad's items reach the user's list, call `acknowledge()` for each one. That is what earns the ad its interaction — the click alone only offers the items. `reportItemsAddedToList()` is separate, and is list telemetry for every item a user adds, ad-sourced or not. Both are usually called for the same item. ```dart void _selectItem(String itemName) { // Earns an add-to-list ad its interaction. Ignored for an item that // belongs to no recently clicked ad, so it is safe to call for everything. _sdk.acknowledge(itemName); // Report the item as added, ad-sourced or not. _sdk.reportItemsAddedToList([itemName], 'My grocery list'); } ``` --- ### Ad Zones # Ad Zones Ad zones are placed by your app. For each zone AdAdapted has allocated to you, place an `AdZone` widget and give it the zone ID. The widget owns everything else about that zone: its own ad request, its own refresh countdown and its own impression reporting. Nothing is shared between zones, so several can be on screen at once, each serving and reporting independently. ## Placing a Zone ```dart import 'package:adadapted_flutter_sdk/adadapted_flutter_sdk.dart'; SizedBox( height: 250, child: AdZone(zoneId: '123456'), ) ``` A zone declares only its own ID. The app ID, session, device info and environment all come from the SDK. The zone starts requesting ads once the SDK has finished initializing, so it is safe to place before `initialize()` resolves. --- ## Sizing An unfilled zone is a `SizedBox.shrink()` and takes up no space at all. A filled one fills whatever space its parent gives it, so **the zone must be bounded** — a zone under unbounded constraints renders nothing visible. Bound it from the parent, as above, or through the widget's own `width` and `height`, which apply only while it has an ad: ```dart AdZone(zoneId: '123456', height: 250) ``` Because an unfilled zone collapses, the space *around* it is yours to manage. Use [onZoneHasAds](#onzonehasads) to collapse padding, dividers or headings alongside it. > **Note:** A filled zone draws a small report this ad affordance in its top right corner, which opens AdAdapted's ad feedback page in an external browser. It is drawn over the creative and needs no space of its own. --- ## Visibility **Zones measure their own visibility.** A zone scrolled out of a list, or left behind by a pushed route, pauses its refresh countdown and stops recording impressions on its own. There is nothing to report and nothing to remember. This is the main difference from the React Native SDK, where `isVisible` is a required prop, and it exists because Flutter can measure what React Native cannot. ### Taking Manual Control `isVisible` is an optional override for the cases measurement cannot see — a zone drawn into a custom compositing layer, or one deliberately kept live behind a transparent overlay. Supply it and the zone stops measuring and believes you instead. ```dart AdZone(zoneId: '123456', isVisible: isCurrentlyVisible) ``` > **Note:** Leave it null unless you have a reason not to. Supplying true for a zone that scrolls or sits under a pushed route over-reports impressions for an ad nobody can see; supplying false and forgetting to update it leaves the zone serving ads that are never reported and never refreshed. --- ## Contextual Ads (Recipes) Recipe context is set per zone, using the `contextId` argument. Set it to the context the zone is currently showing, and clear it by passing `null`. ```dart String? _adContextId; AdZone(zoneId: '123456', contextId: _adContextId) ``` Changing it re-requests the zone's ad against the new context. --- ## Arguments | Argument | Type | Required | Description | | ----------------------- | -------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------- | | `zoneId` | `String` | Yes | The zone ID provided to you by AdAdapted. | | `isVisible` | `bool?` | No | Overrides the zone's own visibility measurement. Leave null to let the zone measure itself. | | `contextId` | `String?` | No | The recipe context this zone is currently showing. Pass `null` to clear it. | | `xyDragDistanceAllowed` | `double?` | No | Touch drag distance, in either direction, still treated as a tap rather than a scroll. Falls back to the value given to `initialize()`, then to `25`. | | `width` | `double?` | No | The width the zone occupies while it has an ad. Null takes whatever width the parent gives it. | | `height` | `double?` | No | The height the zone occupies while it has an ad. Null takes whatever height the parent gives it. | | `onAddToListTriggered` | `void Function(List)` | No | Called when an `add to list` ad in this zone is tapped. | | `onZoneHasAds` | `void Function(bool hasAds)` | No | Called whenever the zone's fill state changes, so you can collapse or reveal the space around it. | | `onAdLoaded` | `VoidCallback` | No | Called when an ad has been retrieved and displayed. | | `onAdLoadFailed` | `VoidCallback` | No | Called when an ad could not be retrieved or displayed. | --- ## Zone Callbacks ### onAddToListTriggered Called when a user taps an `add to list` ad in this zone. Receives the list of items the user selected to add. ```dart AdZone( zoneId: '123456', onAddToListTriggered: (items) { for (final item in items) { // Add the item to the user's list. } }, ) ``` > **Note:** You can handle this per zone here, or once for every zone through the onAddToListTriggered callback on initialize(). A zone without its own handler falls back to that one; the per-zone argument is useful when different zones feed different lists. See [Callbacks: onAddToListTriggered](docs/flutter-callbacks?id=onaddtolisttriggered) for the `DetailedListItem` type definition. > **Note:** After the items reach the user's list, call acknowledge() on your SDK reference for each item, so the interaction is reported. ### onZoneHasAds Called whenever the zone's fill state changes. Use it to collapse the space around a zone that has nothing to show. ```dart bool _zoneHasAds = false; SizedBox( height: _zoneHasAds ? 250 : 0, child: AdZone( zoneId: '123456', onZoneHasAds: (hasAds) { setState(() { _zoneHasAds = hasAds; }); }, ), ) ``` ### onAdLoaded and onAdLoadFailed `onAdLoaded` is called when an ad has been retrieved and displayed. `onAdLoadFailed` is called when an ad could not be retrieved or displayed — for **any** of the reasons under [Unfilled Zones](#unfilled-zones), which it does not distinguish. Treat it as "this zone has nothing to show", and collapse the space around it with [onZoneHasAds](#onzonehasads) rather than trying to diagnose the cause from it. ```dart AdZone( zoneId: '123456', onAdLoaded: () { // An ad is now on screen in this zone. }, onAdLoadFailed: () { // Nothing could be shown for this zone. }, ) ``` --- ## Refreshing Each zone refreshes on its own schedule, using the refresh time served with its current ad. There is nothing to call and nothing to keep in state — the zone swaps its own ad when the countdown elapses. | | Seconds | | --- | --- | | Default, when no usable refresh time is served | 60 | | Shortest honored refresh time | 15 | The countdown freezes while the zone is off screen and while the app is backgrounded, so a zone the user cannot see does not burn through ads. An ad that outlived its refresh time while frozen is replaced on return rather than shown for time nobody saw. --- ## Unfilled Zones A zone with nothing to serve collapses and reports a `zone_unfilled` event carrying one of three reasons. This is the SDK working rather than failing. | Reason | Meaning | | --- | --- | | `no_ad` | The zone exists, but there is no ad to serve it right now. Nothing to fix in the app. | | `request_failed` | The request never returned a usable response. A zone ID that does not exist for your app ID reaches this. | | `render_failed` | An ad was served and the web view could not display it. | > **Note:** The reason is reported to AdAdapted, not to your app. onAdLoadFailed takes no argument, so an integration cannot tell these three apart at runtime — a wrong zone ID and genuinely empty inventory look identical from inside the app. Ask AdAdapted to check the reasons for your zones during integration, or watch the zone_unfilled events through a proxy while testing. --- ## Changing a Zone's ID Changing `zoneId` on a placed widget reports an unmount for the zone being left and a mount for the one arriving, and re-requests. Keying the widget by zone ID avoids the transition entirely by giving each zone its own state: ```dart AdZone(key: ValueKey(zoneId), zoneId: zoneId) ``` --- ### Methods # Methods ## initialize() This is where your SDK session is created. It returns a `Future` that, once complete, leaves your reference to the SDK with a valid session. Once you have a valid session, you will be able to successfully use the other SDK methods. Call it once, from the `initState()` of the widget that owns the SDK. ```dart await sdk.initialize( /// The app ID provided to you by AdAdapted. /// If you have multiple variations of the app, you will be provided with /// multiple App IDs. For each App ID you have, you will need to determine /// the correct one to provide the SDK when initializing based on the user's /// variation of the app. /// Must be provided for the SDK to initialize. appId: String, /// (Optional) /// Provide this value to define what API environment the SDK should use. /// /// Possible Values: /// - Production: ApiEnv.prod /// - Development: ApiEnv.dev /// /// Production is the default value if you don't provide this argument. apiEnv: ApiEnv, /// (Optional) /// A custom advertiser ID to use in place of the IDFA. This doesn't have to /// be an advertiser ID; you can provide any ID that uniquely identifies a /// user. /// /// iOS only, matching the other AdAdapted SDKs. It is ignored on Android, /// where the platform's own advertising ID is always used. advertiserId: String, /// (Optional) /// Adjusts the touch sensitivity of an ad zone in both the X and Y /// directions. This is used to tell a tap on a zone from a scroll: if the /// amount of touch "drag" distance in either direction is less than this /// value, the action is treated as a tap on the zone. /// /// The higher the value, the easier it is for a user to trigger an ad tap. /// If not set, the default is 25. An individual zone can override it /// through its own xyDragDistanceAllowed argument. xyDragDistanceAllowed: double, /// (Optional) /// The store to target ads for, if you are targeting ads by store. storeId: String, /// (Optional) /// Triggered when an "add to list" action has taken place by a user, in any /// ad zone that has no handler of its own. onAddToListTriggered: (items) {}, /// (Optional) /// Triggered when an "add to list" occurs by means of an "out of app" data /// payload. onOutOfAppPayloadAvailable: (payloads) {}, ); ``` **Return Type:** ```dart Future ``` > **Note:** On iOS, request the App Tracking Transparency permission before this call. The tracking status is read once, while device info is gathered, so a permission granted afterwards is not picked up until the next initialize(). > **Note:** Ad zones are not configured here. Place an AdZone widget for each zone AdAdapted has allocated to you, and give it the zone ID. Recipe contexts and manual visibility are per-zone arguments on that widget. --- ## unmount() This ensures the SDK is cleaned up when the widget it is held in is being disposed. Call it from that widget's `dispose()`. It closes out every mounted zone, releases the HTTP client, removes the app lifecycle observer and ends the session. > **Note:** If you do not call this method, you risk creating a memory leak. ```dart sdk.unmount(); ``` **Return Type:** ```dart void ``` --- ## sessionId Once the SDK is initialized, this property holds the current session ID. The session is generated on the device, so the ID is available as soon as `initialize()` resolves. It is `null` before that and after `unmount()`. > **Note:** Sessions are held in memory and never persisted. A relaunch always starts a new session, and only returning to the foreground within 30 minutes resumes the existing one. ```dart sdk.sessionId; ``` **Type:** ```dart String? ``` --- ## deviceInfo Once the SDK is initialized, this property exposes information about the user's current device. It is `null` before `initialize()` resolves and after `unmount()`. ```dart sdk.deviceInfo; ``` **Type:** ```dart DeviceInfo? { /// The unique device ID (GAID on Android, IDFA on iOS). /// Empty when the user has not permitted ad tracking: an /// App Tracking Transparency denial on iOS, limit ad /// tracking or "Delete advertising ID" on Android. Also /// empty where the device has no identifier to give, /// such as an iOS simulator or an AOSP Android image. String udid; /// The device name. String deviceName; /// The operating system name, as reported to the API: /// "android_flutter" or "ios_flutter". String systemName; /// The operating system version. String systemVersion; /// The device model. String deviceModel; /// The device screen width in pixels. String deviceWidth; /// The device screen height in pixels. String deviceHeight; /// The device screen density. String deviceScreenDensity; /// The current device locale. String deviceLocale; /// The device carrier name, or "n/a" when there is none. String deviceCarrier; /// The bundle ID of the host app. String bundleId; /// The bundle version of the host app. String bundleVersion; /// The current device timezone. String deviceTimezone; /// If true, ad tracking is permitted on this device. bool isAdTrackingEnabled; } ``` --- ## acknowledge() This must be called after a user interacts with an `add to list` ad, once the item has actually been added to the user's list. It is what earns the ad its interaction: the tap alone only offers the items, and your app is the only party that knows whether they were accepted. Call it once per item, with the item's `productTitle`. ```dart sdk.acknowledge(itemName); ``` **Return Type:** ```dart void ``` > **Note:** Item names that belong to no recently tapped ad are ignored, so you can safely call this for every item a user adds, ad-sourced or not. > **Note:** This is not the same as reportItemsAddedToList(), which is list telemetry. Both are usually called for the same item. --- ## handleDeepLink() Hands the SDK a deep link your app received. Out-of-app `add to list` payloads can arrive this way. Your app owns its own link routing, so the SDK does not subscribe to the platform's link stream. Call this for every incoming link, including the one that launched the app; anything that is not an AdAdapted payload link is ignored. Payloads extracted from a link are delivered through [onOutOfAppPayloadAvailable](docs/flutter-callbacks?id=onoutofapppayloadavailable). ```dart sdk.handleDeepLink(url); ``` **Return Type:** ```dart void ``` --- ## performKeywordSearch() Once the SDK is initialized, this can be called when performing a search operation within your app. Call it either as the user types in a search term or when the user executes a search after the term is entered. A list of keyword search results is returned that can then be displayed dynamically alongside the search results your app displays to the user. The final result of the search may have many items, in which case you would determine how many of them you would like to display. ```dart sdk.performKeywordSearch(searchTerm); ``` > **Note:** Matching happens entirely on the device. The SDK downloads the list of available keyword terms when it initializes, and performKeywordSearch checks the user's input against that local list. No network request is needed to find a match, so results come back instantly and can be displayed to the user as they type. > > For the purposes of campaign reporting, performKeywordSearch does send a lightweight background event recording the search term and whether it matched. Because search terms are reported, include Search History in your App Store privacy details and in-app search history in Google Play's Data safety form, if not doing so already. **Return Type:** ```dart List [ { /// The search term ID. String termId; /// The search term that was used to validate the provided /// search string against. String term; /// The display string you can use to display the resulting /// item in your search results list. String replacement; /// The display priority of this item amongst all other /// keyword search results. The lower the number, the higher /// the priority. num priority; } ] ``` > **Note:** Sorting is already handled for you by the SDK. The results come back in the correct sort order. The results are pre-sorted by `priority`, lowest first. Terms of equal priority are ordered by the term itself, so the same matches come back in the same sequence on every platform and every call. A search matches only terms that **start with** what the user typed, and the search term must be at least **3 characters** after trimming. Anything shorter returns an empty list and reports nothing. **Search Example:** _che_ | Keyword Term | Priority | Matched | | ------------------- | -------- | ------- | | **Che**x Mix | 2 | Yes | | **Che**ddar Cheese | 3 | Yes | | Mac & **Che**ese | 4 | No — the term does not start with the search | | Sweet **Che**rries | 1 | No — the term does not start with the search | --- ## reportKeywordInterceptTermsPresented() This must be called anytime keyword suggestions (provided by `performKeywordSearch()`) are displayed to the user. Calling it reports those suggested keywords back to AdAdapted. > **Note:** Making sure to call this method when a keyword is displayed will ensure AdAdapted can provide you with the most accurate reporting results. Not every term that satisfied a search has to be presented, so only provide term IDs for the terms that ultimately get presented to the user. See the `performKeywordSearch()` return type for more info on the `termId` property. ```dart sdk.reportKeywordInterceptTermsPresented(termIds); ``` **Return Type:** ```dart void ``` --- ## reportKeywordInterceptTermSelected() This must be called anytime a keyword suggestion (provided by `performKeywordSearch()`) is selected by the user from your displayed search result list. Calling it reports the selected keyword back to AdAdapted. > **Note:** Making sure to call this method when a keyword is selected will ensure AdAdapted can provide you with the most accurate reporting results. This accepts the `termId` from the selected keyword. See the `performKeywordSearch()` return type for more info on the `termId` property. ```dart sdk.reportKeywordInterceptTermSelected(termId); ``` **Return Type:** ```dart void ``` --- ## List Manager This is a feature that allows you to report any items added to, crossed off, or deleted from lists from ads or any other items shown to a user by your app. The following methods allow you to interact with our List Manager feature. Each accepts a list of item name strings, and an optional list name. For ads displayed by this SDK, it is suggested that you send the `productTitle` from the item object. For all other items, you can provide the product name string that is available to you. See the `onAddToListTriggered` type definition for more info on the `DetailedListItem` object that contains the `productTitle` property. The optional list name allows you to associate items to a given list for a user; if no value is provided, those items are associated to a default list for that user. ### reportItemsAddedToList() This must be called when items are added to a user's list. Calling it reports the provided items back to AdAdapted as being added to a list. > **Note:** Making sure to call this method when items are added to a list by the user will ensure AdAdapted can provide you with the most accurate reporting results. ```dart sdk.reportItemsAddedToList(itemNames, listName); ``` **Return Type:** ```dart void ``` ### reportItemsCrossedOffList() This must be called when items are crossed off a user's list. Calling it reports the provided items back to AdAdapted as being crossed off a list. > **Note:** Making sure to call this method when items are crossed off a list by the user will ensure AdAdapted can provide you with the most accurate reporting results. ```dart sdk.reportItemsCrossedOffList(itemNames, listName); ``` **Return Type:** ```dart void ``` ### reportItemsDeletedFromList() This must be called when items are deleted from a user's list. Calling it reports the provided items back to AdAdapted as being deleted from a list. > **Note:** Making sure to call this method when items are deleted from a list by the user will ensure AdAdapted can provide you with the most accurate reporting results. ```dart sdk.reportItemsDeletedFromList(itemNames, listName); ``` **Return Type:** ```dart void ``` --- ## Out-Of-App Payload Data This is a feature that allows you to listen for items a user wants to add to their list from ads being served external to your app. The following are the two types of payloads that can occur: - **Deep Links:** If your app allows for deep links to navigate back to your app, pass each incoming link to [handleDeepLink()](#handledeeplink) and the SDK will extract the payload data from any AdAdapted link among them. - **Payload Data API:** An API call is made to AdAdapted's Payload Data server to check and see if there are any pending payload data requests for the user. This is triggered when the SDK is initialized, and any time the app returns to the foreground. A single callback is used to intercept the payload data for either of the two approaches above. For detailed information on how to implement it, see [Callbacks: onOutOfAppPayloadAvailable](docs/flutter-callbacks?id=onoutofapppayloadavailable). Once you have received the payload data and processed each provided payload, you will need to call one of the two following methods to confirm that you processed or rejected the payload: ### markPayloadContentAcknowledged() This allows AdAdapted to confirm the payload was received, and the items were processed into the user's list. It accepts the `payloadId`. You must trigger this method for each accepted payload you process. > **Note:** You must call this method if the items were processed into the user's list for a particular payload. If you don't, the next time the user loads your app, the payload will still be available and retrieved again. ```dart sdk.markPayloadContentAcknowledged(payloadId); ``` **Return Type:** ```dart void ``` ### markPayloadContentRejected() This allows AdAdapted to confirm the payload was received, and the items were rejected from being added to the user's list. You can use this if the items in the payload already exist in the user's list. It accepts the `payloadId`. You must trigger this method for each rejected payload you process. > **Note:** You must call this method if the items were rejected from being added to the user's list for a particular payload. If you don't, the next time the user loads your app, the payload will still be available and retrieved again. ```dart sdk.markPayloadContentRejected(payloadId); ``` **Return Type:** ```dart void ``` --- ## Ad Zones Ad zones are placed by your app, one `AdZone` widget per zone. Off-screen zones, zone visibility and recipe contexts are all handled through that widget's arguments rather than through methods on the SDK. See [Ad Zones](docs/flutter-ad-zones.md) for the full reference. --- ### Callbacks # Callbacks These callbacks are provided to `initialize()` and apply across every ad zone. Individual zones can also report `add to list` taps, fill state and load results through their own arguments — see [Ad Zones: Zone Callbacks](docs/flutter-ad-zones?id=zone-callbacks). ## onAddToListTriggered This callback is triggered when a user taps an `add to list` ad zone. It receives an `items` list that contains each item the user selected to add to their list. If there are any other actions you need to perform when the user adds an ad item to their list, you can do so here. > **Note:** A zone with its own onAddToListTriggered handler uses that instead. This one is the fallback for every zone that has none. > **Note:** After the items reach the user's list, call acknowledge() for each item so the interaction is reported. ```dart sdk.initialize( // ...other arguments onAddToListTriggered: (items) { // See below for the type definition of the "items" list. }, ); ``` **Type Definition:** _items_ ```dart List [ { /// The barcode of the product. String productBarcode; /// The brand of the product. String productBrand; /// The category of the product. String productCategory; /// The discount given for the product. String productDiscount; /// The image used for display of the product. String productImage; /// The SKU of the product. String productSku; /// The name/title of the product. String productTitle; /// The tracking ID, when the API supplied one. String? trackingId; } ] ``` --- ## onOutOfAppPayloadAvailable This callback is triggered when an `out-of-app payload` is available for processing. This allows you to process all available payloads, each including a list of products a user would like to add to their list. Payloads reach this callback from two sources: the Payload Data server, which the SDK checks at initialization and whenever the app returns to the foreground, and any AdAdapted payload link your app hands to [handleDeepLink()](docs/flutter-methods?id=handledeeplink). If there are any other actions you need to perform when `out-of-app payload` data becomes available to process, you can do so here. ```dart sdk.initialize( // ...other arguments onOutOfAppPayloadAvailable: (payloads) { // See below for the type definition of the "payloads" list. }, ); ``` **Type Definition:** _payloads_ ```dart List [ { /// The payload ID associated to the provided list items. String payloadId; /// The payload message. String? payloadMessage; /// The payload image. String? payloadImage; /// The campaign ID. String? campaignId; /// The app ID. String? appId; /// Expiration time in seconds. num? expireSeconds; /// The list of products to be added to a list. List detailedListItems; } ] ``` See [onAddToListTriggered](#onaddtolisttriggered) for the `DetailedListItem` type definition. > **Note:** When processing the payloads, make sure to finish by calling either markPayloadContentAcknowledged() or markPayloadContentRejected() after each payload is processed. ```dart onOutOfAppPayloadAvailable: (payloads) { for (final payload in payloads) { for (final item in payload.detailedListItems) { // Add the item to the user's list. } // Mark this payload as acknowledged. sdk.markPayloadContentAcknowledged(payload.payloadId); } }, ``` --- ### Caveats # Caveats ## Android and iOS Only The plugin registers platform implementations for Android and iOS only. There is no web, macOS, Windows or Linux support, so a build targeting one of those has no device info, no advertising identifier and no ad tracking status to work with. --- ## Two Ad Action Types Are Not Handled The SDK actions two of the four ad action types: | Ad type | `action_type` | Handled | | --- | --- | --- | | Add to list | `c` | Yes — the items reach your `onAddToListTriggered` handler | | Standard external link | `e` | Yes — `action_path` opens in an external browser | | In-app popup link | `l` | No | | App store link | `a` | No | An ad served with an action type the SDK does not handle stays on screen and does nothing when tapped, rather than costing the zone its ad. Nothing is reported for the tap, so a served impression can earn no interaction. > **Note:** If your campaigns use the l or a action types, talk to AdAdapted before shipping a Flutter integration — the same campaign behaves differently here than it does on the native Android and iOS SDKs. --- ## Ad Action Paths Are Restricted by Scheme An external link ad opens its `action_path` only when the URL parses and uses `https`, `http`, or a store scheme (`market`, `itms-apps`, `itms-appss`). Anything else is refused and nothing is opened. The tap is still reported as an interaction and the zone still refreshes, exactly as when the device has no handler for the URL — a refused path costs the user a tap that goes nowhere, not the zone its ad. Nothing in your app has to handle this; it is noted so a campaign that does not open is diagnosed against the creative's `action_path` rather than the integration. --- ## App Tracking Transparency Must Be Requested First On iOS, your app must request the App Tracking Transparency permission **before** calling `initialize()`. The tracking status is read once, while device info is gathered, so a permission granted after that is not picked up until the next `initialize()`. Your app also needs an `NSUserTrackingUsageDescription` purpose string in `ios/Runner/Info.plist` before it can prompt at all. See [Getting Started: iOS](docs/flutter-getting-started?id=ios). --- ## advertiserId Is iOS Only The optional `advertiserId` given to `initialize()` replaces the IDFA on iOS. It is **ignored on Android**, where the platform's own Google Advertising ID is always used. This matches the other AdAdapted SDKs. A build that passes one therefore reports the custom ID on iOS and a GAID on Android, which is expected rather than a defect. --- ## Advertising Identifiers on Emulators Where there is no advertising identifier the SDK reports an empty string rather than substituting something else, on both platforms. - **Android** — a **Google Play** or **Google APIs** emulator image supplies a working advertising ID. An AOSP image has none at all. The SDK reports an empty identifier whenever the user has opted out — *Delete advertising ID* in **Settings → Google → Ads** on Android 12 and above, or *Opt out of Ads Personalization* below it — on every API level the SDK supports, not only those where the platform zeroes the ID itself. - **iOS** — the simulator has no IDFA to hand out. There is nothing behind the App Tracking Transparency permission there, so a real integration reads empty. Verify anything that depends on a *production* identifier — attribution, retargeting — on physical hardware. --- ## Sessions Do Not Survive a Relaunch Sessions are generated on the device, held in memory and never persisted. A relaunch always starts a new session; only returning to the foreground within 30 minutes resumes the existing one. > **Note:** This is expected behavior rather than a defect, but it is worth knowing when reading reports or debugging: a session ID you saw before restarting the app will not come back. --- ## An Ad Zone Must Be Bounded An unfilled zone is a `SizedBox.shrink()` and takes up no space. A filled one fills whatever space its parent gives it, so a zone under unbounded constraints — the common case being a `Column` or a scrolling list with no height around it — renders nothing visible. Give it bounds from the parent, or through the widget's own `width` and `height`. See [Ad Zones: Sizing](docs/flutter-ad-zones?id=sizing). --- ## One SDK Instance Per App Create a single `AdadaptedFlutterSdk` and share it. Hold it in the state of a widget high enough in the tree that it is not rebuilt, and call [unmount()](docs/flutter-methods?id=unmount) from that widget's `dispose()`. A second instance does not coexist with the first — **it takes over every zone in the app**. Ad zones read their session, app ID and device info from a single shared registry, and the most recent `initialize()` replaces what is in it. Zones placed for the first instance keep rendering, but from that point they request and report under the second instance's session and app ID. > **Note:** This matters most in a modular app, where two packages can each own an SDK instance without either knowing about the other. The result is not a duplicate session; it is one instance's zones reporting as the other, which is cross-contaminated data that cannot be separated afterwards. --- ## Android SDK ### Getting Started # Getting Started ## Features - Ad zone creation - In-app ad display popup/browser - In-list add suggestion based on available keywords - Event tracking enabling reporting - Supported ad types: - Add to list (reports a product to add to a list) - Add to list circular (opens in-app within a popup view and displays a multi-item "add to list" circular) - Add to list from outside the app (Deeplinks etc.) - Standard external link (opens in external browser) - Popup external link (opens in-app within popup view) - Context based ads determined by a recipe or context ID ## Breaking Version Updates > **Note:** As of version 5.0.0 the AaSdkSessionListener callback has been removed (use individual zone listeners instead). > > As of version 4.0.1 the namespaces for each class in the sdk have changed. Simply remove the old namespaces and import the new ones. Additionally, the minimum Android SDK version has been increased to 23, and the target version is now 34. If your application is pointing to a different minimum/target version, you may need to make further updates to align with these changes. This also utilizes gradle plugin version 8.0+ and JDK 17. > > As of version 3.1.x both Keyword Intercepts and External Payloads must be specifically enabled in the main init block of the SDK. Please see the Usage section for more information. > > As of version 2.3.x the targetSdkVersion was bumped to version 30 (Android 11) and the minimumSdkVersion up to 19 (KitKat) which may require some dependency updates within your application. > > As of version 2.2.11 any application that utilizes an obfuscation tool should exempt the AdAdapted SDK from obfuscation. (i.e. for Proguard do the following in your proguard-rules.pro file: -keep class com.adadapted.android.sdk.** { *; } ## Installation To add the AdAdapted Android SDK to your app, simply add the following repository url to your **root build.gradle**: ```groovy allprojects { repositories { ... maven { url 'https://jitpack.io' } } } ``` --- Then add the dependency (version based on latest jitpack release): ```groovy dependencies { implementation 'com.github.adadaptedinc:android-sdk:X.X.X' } ``` ## License [AdAdapted Platform License](docs/license.md) --- ### Usage ## Initialization There are only a few steps required to initialize the SDK. We recommend creating your own class that extends ``Application``. The following guide assumes that this is what you have done, but you can also initialize the SDK inside your primary launcher activity. Inside the ``onCreate()`` method of your class definition, add the following lines: #### ** Kotlin ** ```kotlin override fun onCreate() { // ... AdAdapted.withAppId("API_KEY") .inEnv(Env.DEV) .start(this) } ``` #### ** Java ** ```java @Override public void onCreate() { // ... AdAdapted.INSTANCE .withAppId("API_KEY") .inEnv(AdAdapted.Env.DEV) .start(this); } ``` > **Note:** Replace the API_KEY placeholder string in the code above with the key provided to you by AdAdapted. Also, when your app is ready for release, change AdAdapted.Env.DEV to AdAdapted.Env.PROD. You will now need to modify the **AndroidManifest.xml** file. To start, update your permissions to include the following: ```xml ``` --- > **Note:** Our SDK utilizes the AD_ID permission to properly record impressions for ads and serve them to users. There is no change required for your app to allow this but is something to be aware of for app distribution and privacy notes. You will also be asked within the Google Play Console if your app implements an SDK that utilizes the Advertising ID, in which case it now does. Next, within your Application class, make sure to add the following attribute inside the ```` tag: ```xml //... ``` --- Finally, within the ```` tag, add this activity: ```xml ``` --- > **Note:** The last code snippet above ensures that any pop-up based content ads will work properly when clicked. ## Keyword Intercept and Payloads Please note that as of version 3.1.0 it is necessary to enable both Keyword Intercept and External Payload functionality manually to activate these features. They can be turned on and off by appending them to the init call below: #### ** Kotlin ** ```kotlin override fun onCreate() { // ... AdAdapted.withAppId("API_KEY") .inEnv(Env.DEV) .enableKeywordIntercept(true) .enablePayloads(true) .start(this) } ``` #### ** Java ** ```java @Override public void onCreate() { // ... AdAdapted.INSTANCE .withAppId("API_KEY") .inEnv(AdAdapted.Env.DEV) .enableKeywordIntercept(true) .enablePayloads(true) .start(this); } ``` ## Additional Builder Options The following optional methods can be chained onto the SDK initialization call: #### ** Kotlin ** ```kotlin AdAdapted.withAppId("API_KEY") .inEnv(Env.DEV) .enableDebugLogging() // Enables verbose SDK logging for development .setCustomIdentifier("your_custom_id") // Sets a custom user identifier for targeting .setOptionalParams(hashMapOf("key" to "value")) // Passes additional parameters to the SDK .start(this) ``` #### ** Java ** ```java HashMap params = new HashMap<>(); params.put("key", "value"); AdAdapted.INSTANCE .withAppId("API_KEY") .inEnv(AdAdapted.Env.DEV) .enableDebugLogging() // Enables verbose SDK logging for development .setCustomIdentifier("your_custom_id") // Sets a custom user identifier for targeting .setOptionalParams(params) // Passes additional parameters to the SDK .start(this); ``` | Method | Description | | ------ | ----------- | | `enableDebugLogging()` | Enables verbose debug logging from the SDK. Useful during development and should be removed before release. | | `setCustomIdentifier(identifier)` | Sets a custom user identifier that will be used for ad targeting instead of the device's default identifier. | | `setOptionalParams(params)` | Passes a map of additional key-value parameters to the SDK for custom targeting or configuration. | ## Privacy Controls If a user decides to not share their advertising ID or opts out of all ad related tracking, you should push that preference through to the SDK to completely disable tracking functionality. Simply call the `AdAdapted.disableAdTracking(context)` method prior to the initialization of the SDK (but can be later if not possible). This should only need to be called once, as it will store the preference in `SharedPreferences` on the device. If the user preference should change, the option to enable ad tracking again is also available via `AdAdapted.enableAdTracking(context)`. #### ** Kotlin ** ```kotlin override fun onCreate() { // ... AdAdapted.disableAdTracking(this) AdAdapted.withAppId("API_KEY") .inEnv(Env.DEV) .start(this) } ``` #### ** Java ** ```java @Override public void onCreate() { // ... AdAdapted.INSTANCE.disableAdTracking(this); AdAdapted.INSTANCE .withAppId("API_KEY") .inEnv(AdAdapted.Env.DEV) .start(this); } ``` --- ### Classes ## AaZoneView A zone is an area in which an ad is displayed. A zone has a numeric ID which is provided by AdAdapted and exposed to the SDK to enable ads to be served to the zone. To setup an `AaZoneView`, start by adding the following view to one of your visible layouts: ```xml ``` Next, using the correct approach below, update your `onCreate()` method to assign this new view a zone ID upon initialization. This associates the zone with an ad served by the AdAdapted platform. The AdZone can also be entirely created programmatically. #### ** Kotlin ** ```kotlin val mainActivityAd = findViewById(R.id.mainActivityAd) // Substitute zone_id for your own numeric zone ID mainActivityAd.init("zone_id") ``` #### ** Java ** ```java AaZoneView mainActivityAd = (AaZoneView)findViewById(R.id.mainActivityAd); // Substitute zone_id for your own numeric zone ID mainActivityAd.init("zone_id"); ``` Finally, within **MainActivity**, you should add the following two lifecycle methods: #### ** Kotlin ** ```kotlin override fun onStart() { // ... mainActivityAd.onStart() } override fun onStop() { mainActivityAd.onStop() // ... } ``` #### ** Java ** ```java @Override public void onStart() { // ... mainActivityAd.onStart(); } @Override public void onStop() { mainActivityAd.onStop(); // ... } ``` > **Note:** We recommend using an AndroidView to wrap our Ad Zone control if using compose based UI layouts. ## Off-Screen Ad Zone Visibility If your ad zone is not immediately visible on screen (e.g., it requires scrolling to be seen), you can control the visibility state to ensure accurate ad tracking. Set the visibility to `false` before starting the zone, then update it as the zone scrolls into or out of view: #### ** Kotlin ** ```kotlin mainActivityAd.setAdZoneVisibility(false) ``` #### ** Java ** ```java mainActivityAd.setAdZoneVisibility(false); ``` ## Context Based Ads (Recipes) If you're using our context/recipe implementation, you can use the following methods on the `AaZoneView` to enable a context mode for ad zones. This will refresh the ads within the zone with content that's relevant to a specific ID sent to us. Keep in mind, this feature only works with valid context IDs. If you're interested in this feature and haven't set it up yet, feel free to reach out to us for more information on how to get started. ```kotlin //AaZoneView Class //Sets the current context of the zone and refreshes the ads. fun setAdZoneContextId(contextId: String) //Removes this specific zone's context and refreshes to show default ads. fun removeAdZoneContext() ``` ## AdContentListener Some ads can insert items directly into your lists if desired. To do this, use the activity or fragment where the ad interaction is taking place to implement an `AdContentListener`. First, using the correct approach below, implement `AdContentListener` as follows: #### ** Kotlin ** ```kotlin class MainActivity : AppCompatActivity(), AdContentListener { // ... } ``` #### ** Java ** ```java public class MainActivity extends AppCompatActivity implements AdContentListener { // ... } ``` Next, you'll need to override the `onContentAvailable()` method as follows: #### ** Kotlin ** ```kotlin override fun onContentAvailable(zoneId: String, content: AddToListContent) { val items = content.items for (item in items) { Log.d(TAG, "AddToListItem: " + item.title) // Acknowledge the item(s) added to the list content.itemAcknowledge(item) } // Acknowledge content was delivered (not necessary if acknowledging per item as above) content.acknowledge() } ``` #### ** Java ** ```java @Override public void onContentAvailable(final String zoneId, final AddToListContent content) { final List items = content.getItems(); for (final AddToListItem item : items) { Log.d(TAG,"AddToListItem: " + item.getTitle()); // Acknowledge the item(s) added to the list content.itemAcknowledge(item); } // Acknowledge content was delivered (not necessary if acknowledging per item as above) content.acknowledge(); } ``` The `AddToListContent` object will contain a list of `AddToListItem` and can be accessed using `content.items` or `content.getItems()` based on your used approach above. Each `AddToListItem` contains several pieces of information about the product in the current ad (product title, image url, etc.). The calls to `content.itemAcknowledge()` and `content.acknowledge()` in the code snippet above are necessary to acknowledge that the user clicked on the ad and the product was added to the list. > **Note:** While use of itemAcknowledge() is preferred, the standard acknowledge() method is still valid. This call should be made at the time the product is added to the list, as opposed to when the item is first clicked. > **Note:** There is also a method called onNonContentAction(String zoneId, String adId) that can be used specifically to listen for action events on ads that do not produce content. An example of this could be external linking ads or pop-up view ads that display information but do not add anything to the user's cart. If the payload was processed, but the product was not added to the list for whatever reason, you can indicate this using one of the following two methods: #### ** Kotlin ** ```kotlin content.itemFailed(item, "Reason for not adding product to list") ``` #### ** Java ** ```java content.itemFailed(item, "Reason for not adding product to list"); ``` or #### ** Kotlin ** ```kotlin content.failed("Reason for adding NO ITEMS to list") ``` #### ** Java ** ```java content.failed("Reason for adding NO ITEMS to list"); ``` Finally, you'll need to add and remove the listener in the lifecycle methods. This is achieved by passing an `AdContentListener` into the `onStart()` and `onStop()` methods as follows: #### ** Kotlin ** ```kotlin override fun onStart() { // ... mainActivityAd.onStart(this) } override fun onStop() { mainActivityAd.onStop(this) // ... } ``` #### ** Java ** ```java @Override public void onStart() { // ... mainActivityAd.onStart(this); } @Override public void onStop() { mainActivityAd.onStop(this); // ... } ``` > **Note:** The above code is implemented when setting up an AaZoneView. You just need to pass in this. > **Note:** If you would also like to listen to ad interactions that do not have items or content to deliver (such as external browser linking ads), there is an optional method you can listen for as well called onNonContentAction(). This method provides the ad zone's ID and the ad's ID itself that you can use for any personal data points. If you're using an older version of Java this method will require an override and implementation, although it can be left blank if not used. ## AdAdaptedListManager Use the `AdAdaptedListManager` to acknowledge when an item has been added to, crossed off, or removed from a list. Whenever a product is **added to** a list, acknowledge this by calling the `itemAddedToList()` method as follows: #### ** Kotlin ** ```kotlin AdAdaptedListManager.itemAddedToList("listName", "added_product_name") ``` #### ** Java ** ```java AdAdaptedListManager.INSTANCE.itemAddedToList("listName", "added_product_name"); ``` Items may be **crossed off** (purchased / completed) the list by the user. Acknowledge this by calling the `itemCrossedOffList()` method as follows: #### ** Kotlin ** ```kotlin AdAdaptedListManager.itemCrossedOffList("listName", "crossed_off_product_name") ``` #### ** Java ** ```java AdAdaptedListManager.INSTANCE.itemCrossedOffList("listName", "crossed_off_product_name"); ``` Items may be **removed from** (no longer needed) the list by the user. Acknowledge this by calling the `itemDeletedFromList()` method as follows: #### ** Kotlin ** ```kotlin AdAdaptedListManager.itemDeletedFromList("listName", "deleted_product_name") ``` #### ** Java ** ```java AdAdaptedListManager.INSTANCE.itemDeletedFromList("listName", "deleted_product_name"); ``` > **Note:** All of the methods above do not require a list name and can be called with just the item name insead: AdAdaptedListManager.itemAddedToList(item = "added_product_name"). ## SDK Notifications There are occasions when no ads are available to return at the time of your request. However, notifications are available at runtime should you want to check that there are ads to serve. The following table lists the available notification types: | Type | Description | | ---- | ----------- | | Zone | An `AaZoneView` notification that returns whether a _specific zone_ has ads to serve | ### AaSdkSessionListener > **Note:** Removed in version 5.0.0: AaSdkSessionListener and setSdkSessionListener() no longer exist in the SDK. Use the per-zone onZoneHasAds() callback on AaZoneView.Listener below to find out whether a zone has ads to serve. ### AaSdkEventListener To listen to all generic ad events you can setup the `AaSdkEventListener` to trigger for each ad event type. You'll need to implement this listener in your SDK initialization chain: #### ** Kotlin ** ```kotlin AdAdapted .withAppId("API_KEY") .inEnv(AdAdapted.Env.DEV) .setSdkEventListener(object : AaSdkEventListener { override fun onNextAdEvent(zoneId: String, eventType: String) { Log.i(tag, "Ad $eventType for Zone $zoneId") } }) .start(this) ``` #### ** Java ** ```java AdAdapted.INSTANCE .withAppId("API_KEY") .inEnv(AdAdapted.Env.DEV) .setSdkEventListener(new AaSdkEventListener() { @Override public void onNextAdEvent(String zoneId, String eventType) { Log.i(TAG, "Ad " + eventType + " for Zone " + zoneId); } }) .start(this); ``` ### AaZoneView.Listener To listen for **Zone** level notifications, you should register `AaZoneView.Listener` as a parameter to the `onStart()` method for your `AaZoneView` as follows: #### ** Kotlin ** ```kotlin mainActivityAd.onStart(object : AaZoneView.Listener { override fun onZoneHasAds(hasAds: Boolean) { Log.d(TAG, "Zone has ads available: $hasAds") } override fun onAdLoaded() { } override fun onAdLoadFailed() { } }, this) ``` #### ** Java ** ```java mainActivityAd.onStart(new AaZoneView.Listener() { @Override public void onZoneHasAds(boolean hasAds) { Log.d(TAG, "Zone has ads available: " + hasAds); } @Override public void onAdLoaded() { } @Override public void onAdLoadFailed() { } }, this); ``` The methods `onAdLoaded()` and `onAdLoadFailed()` shown in the snippets above tell you whether an ad that was already fetched was able to display in the zone's web view. `onAdLoadFailed()` means the web view couldn't load the ad's creative, usually because of bad HTML or a problem loading something the creative references. It does not fire when the ad request to AdAdapted fails. These events are rare and these methods should only be used in very specific circumstances. As of version 6.0.0 `onZoneHasAds()` is reported every time the zone gets a response, not just on the first load. If a refresh comes back with no ad available, it reports `false`. If a later refresh comes back with an ad, it reports `true` again. Keep listening for as long as the zone is on screen and collapse or restore its space in your layout to match, so an empty zone doesn't leave a gap in your UI. The callback comes back on the main thread, so it's safe to update your layout right there. Keep in mind that this only covers responses we actually get back. If the ad request itself fails because of a network or server error, the zone renders blank without reporting `false` and without calling `onAdLoadFailed()`. That means `onZoneHasAds()` on its own isn't enough to decide whether to reserve the zone's space. A zone only counts down to its next refresh while it's attached, visible, in the window, and the app is in the foreground. Setting the `AaZoneView` or any view above it to `GONE` or `INVISIBLE` detaches the zone and pauses that countdown, so a hidden zone won't refresh and can't report `true` on its own. Once you make it visible again, the zone re-attaches and fetches right away if it has no ad to show, then reports `onZoneHasAds()` with the result. Bring the zone back based on something in your own app rather than waiting on a callback, since a hidden zone can't send one. > **Note:** If onZoneHasAds() reports false for a given zone ID, no ad is loaded for that response and the zone renders blank. The zone keeps refreshing while it's on screen, so a later response can still report true. ## KeywordInterceptMatcher Once the SDK is initialized, you can use the `KeywordInterceptMatcher` when performing a search operation within your app. Call the `match()` function either as the user types in a search term or when the user executes a search after the term is entered. A list of keyword search results in the form of `Suggestion` will be returned by this method that can then be used to display dynamically along with the search results your app displays to the user. The final result of the search may have many items, in which case you would determine how many of them you would like to display. > **Note:** Breaking Change: As of version 2.2.1 it is no longer necessary to create a KeywordInterceptMatcher object first- it can now be called directly. Any previous instantiations of this object can be safely removed. As an example, inside of an `EditText` object you can override `afterTextChanged()` and utilize the `KeywordInterceptMatcher` as follows: #### ** Kotlin ** ```kotlin fun afterTextChanged(s: Editable) { val entry: String = editText.getText().toString() var suggestions = KeywordInterceptMatcher.match(entry) } ``` #### ** Java ** ```java @Override public void afterTextChanged(Editable s) { final String entry = editText.getText().toString(); final Set suggestions = KeywordInterceptMatcher.INSTANCE.match(entry); } ``` > **Note:** Sorting based on priority is already handled for you by the SDK. The results will already be in the correct sort order. The final result of suggestions is pre-sorted based on the following criteria: - First, the suggestions are sorted based on which terms "start" with the characters in the provided search string. - Second, they are then sorted by a pre-determined priority for each keyword that satisfied the search. There are two additional methods on the `Suggestion` objects that should be called when using Keyword Intercept. Whenever a suggestion has been displayed, make sure to call `presented()` to acknowledge the keyword intercept item was presented to the user as follows: #### ** Kotlin ** ```kotlin suggestion.presented() ``` #### ** Java ** ```java suggestion.presented(); ``` If the user selects the presented suggestion, this action should be acknowledged using the `selected()` method as follows: #### ** Kotlin ** ```kotlin suggestion.selected() ``` #### ** Java ** ```java suggestion.selected(); ``` > **Note:** Making sure to call these methods when a keyword is displayed/selected will ensure AdAdapted can provide you with the most accurate reporting results. ## AaSdkAdditContentListener Implementing out-of-app Add-It capability allows ads served in other mobile applications or deeplinks to direct the user back into the list app to add the product to their shopping list. Functionally, you'll just need to set up an `AaSdkAdditContentListener` to watch for outside content payloads and handle them appropriately. To initialize this functionality, you will first need to make an update to **AndroidManifest.xml**. Inside the `` tag, add the following: ```xml ``` > **Note:** As of 4.X.X the activity name parameter has changed to com.adadapted.android.sdk.core.deeplink.DeeplinkInterceptActivity instead of com.adadapted.android.sdk.ui.activity.AdditInterceptActivity. Next, add the following method call to your SDK initialization chain to set up the handling for the payload: #### ** Kotlin ** ```kotlin AdAdapted .withAppId("API_KEY") .inEnv(AdAdapted.Env.DEV) .setSdkAdditContentListener(AaSdkAdditContentListener { content -> Log.d(TAG, "Out-of-app content available") val listItems = content.items for (item in listItems) { Log.d(TAG, "AddToListItem: " + item.title) // Handle incoming content // Acknowledge the item(s) added to the list content.itemAcknowledge(item); } // Acknowledge payload was delivered (not necessary if acknowledging per item as above) content.acknowledge() }) .start(this) ``` #### ** Java ** ```java AdAdapted.INSTANCE .withAppId("API_KEY") .inEnv(AdAdapted.Env.DEV) .setSdkAdditContentListener(new AaSdkAdditContentListener() { @Override public void onContentAvailable(AddToListContent content) { Log.d(TAG, "Out-of-app content available"); final List items = content.getItems(); for (final AddToListItem item : items) { Log.d(TAG, "AddToListItem: " + item.getTitle()); // Handle incoming content // Acknowledge the item(s) added to the list content.itemAcknowledge(item); } // Acknowledge payload was delivered (not necessary if acknowledging per item as above) content.acknowledge(); } }) .start(this); ``` **Other Delivery Methods for Out-of-App Payload Ads** ![Out-Of-App Ad Payload Test](../images/out-of-app-ad-payload-test.png) When beginning development on out-of-app payload ads, please work with our team to confirm that a payload and test link has been prepared for your app. Once you are ready to begin the testing phase, this app-specific link can be used to schedule a payload or fire a deeplink: - **Schedule a Payload**: From a web browser on a computer or the test device, navigate to the test link URL and enter the device's advertising ID. In Android, this can be found in **Settings -> Google -> Ads**. Then click the "Schedule" button. The next time your app is opened the payload will be processed as defined in the app. - **Fire Deeplink**: From a web browser on the test device, navigate to the test link URL and click the "Fire Deeplink" button. Your app should open immediately and the payload will be processed as defined in the app. After processing, the browser window will be re-opened. --- ## iOS SDK (Swift Package) ### Getting Started # Getting Started ## Features - Ad zone creation for both storyboard and SwiftUI components - In-app ad display popup/browser - In-list add suggestion based on available keywords - Event tracking enabling reporting - Full support for Objective-C based applications - Supported ad types: - Add to list (reports a product to add to a list) - Add to list circular (opens in-app within a popup view and displays a multi-item "add to list" circular) - Standard external link (opens in external browser) - Popup external link (opens in-app within popup view) - Context based ads determined by a recipe or context ID ## Installation To add the AdAdapted iOS SDK to your app, simply add the package to your XCode project via Swift Package Manager and import the library as such: #### ** Swift ** ```swift import adadapted_swift_sdk ``` #### ** Objective-C ** ```objectivec @import adadapted_swift_sdk; ``` > **Note:** Breaking Changes: > > As of version 3.0.0 the AaSdkSessionListener callback has been removed (use individual zone listeners instead). ## Coming from the old sdk? If you're looking for a quicker way to get into the new SPM sdk and would like a general upgrade guide, view it here: [iOS SPM Upgrade Guide](docs/ios-upgrade-guide.md) ## Coming from an Objective-C app? If you're upgrading from the Objective-C SDK or integrating the Swift Package SDK into an existing Objective-C app, view the dedicated guide here: [Objective-C Upgrade Guide](docs/ios-upgrade-guide.md?id=upgrading-from-objective-c-sdk) ## License [AdAdapted Platform License](docs/license.md) Looking for the Legacy iOS SDK? [Legacy Documentation](docs/ios-old-sdk.md) --- ### Usage # Initialization There are only a few steps required to initialize the SDK once it's imported. First start by accessing the `AdAdapted` class inside of your `AppDelegate` class. In your ``application()`` method add the following lines: #### ** Swift ** ```swift @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate, AaSdkEventListener, AaSdkAdditContentListener { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { AdAdapted .withAppId(key: "API_KEY") // Replace the API_KEY with the key provided to you .inEnv(env: AdAdapted.Env.DEV) // When your app is ready for release, change this flag to PROD .enableKeywordIntercept(value: true) //OPTIONAL: Enable keyword intercept if you plan on using this feature .enablePayloads(value: true) //OPTIONAL: Enable payload feature if needed .enableDebugLogging() //OPTIONAL: Enable for extra debug information .setSdkEventListener(listener: self) //OPTIONAL: Enable to listen for generic events .setSdkAdditContentListener(listener: self) //OPTIONAL: Enable to listen for content events .start() return true } } ``` #### ** Objective-C ** ```objectivec // AppDelegate.h @import adadapted_swift_sdk; @interface AppDelegate : UIResponder @end // AppDelegate.m @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [AdAdapted withAppId:@"API_KEY"]; // Replace with your API key [AdAdapted inEnvironmentProduction:NO]; // Change to YES for release [AdAdapted enableKeywordIntercept:YES]; // OPTIONAL [AdAdapted enablePayloads:YES]; // OPTIONAL [AdAdapted enableDebugLogging]; // OPTIONAL [AdAdapted setSdkEventListener:self]; // OPTIONAL [AdAdapted setSdkAdditContentListener:self]; // Necessary for Add To List [AdAdapted start]; return YES; } @end ``` > **Note:** Replace the API_KEY placeholder string in the code above with the key provided to you by AdAdapted. Also, when your app is ready for release, change AdAdapted.Env.DEV to AdAdapted.Env.PROD (Swift) or inEnvironmentProduction:NO to inEnvironmentProduction:YES (Objective-C). --- ### Classes ## AaZoneView A zone is an area in which an ad is displayed. A zone has a numeric ID which is provided by AdAdapted and exposed to the SDK to enable ads to be served to the zone. To setup an `AaZoneView`, start by adding the view to one of your storyboards accompanied by a reference to it in your ViewController: #### ** Swift ** ```swift @IBOutlet weak var aaZoneView: AaZoneView! ``` #### ** Objective-C ** ```objectivec @property (nonatomic, strong) AaZoneView *aaZoneView; ``` Next, using the correct approach below, update your `viewDidLoad()` method to assign this new view a zone ID upon initialization. This associates the zone with an ad served by the AdAdapted platform. You'll also want to call `onStart()`, passing in listeners as well, shortly after initializing it if not immediately. #### ** Swift ** ```swift // Substitute zone_id for your own numeric zone ID aaZoneView.initialize(zoneId: "zone_id") aaZoneView.onStart(listener: self, contentListener: self) ``` #### ** Objective-C ** ```objectivec // Substitute zone_id for your own numeric zone ID [self.aaZoneView initializeWithZoneId:@"zone_id"]; [self.aaZoneView startWithZoneViewListener:self adContentListener:self]; ``` Finally, within your ViewController, you should add the following cleanup method when your view is destroyed or removed to ensure the zone's listeners are properly destroyed: #### ** Swift ** ```swift override func viewDidDisappear(_ animated: Bool) { aaZoneView.onStop() } ``` #### ** Objective-C ** ```objectivec - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; [self.aaZoneView onStop]; } ``` ## Off-Screen Ad Zone Visibility If your ad zone is not immediately visible on screen (e.g., it requires scrolling to be seen), you can control the visibility state to ensure accurate ad tracking. Set the visibility to `false` before starting the zone, then update it as the zone scrolls into or out of view: #### ** Swift ** ```swift aaZoneView.setAdZoneVisibility(isViewable: false) ``` #### ** Objective-C ** ```objectivec [self.aaZoneView setAdZoneVisibility:NO]; ``` ## Context Based Ads (Recipes) If you're using our context/recipe implementation, you can use the following methods on the `AaZoneView` to enable a context mode for ad zones. This will refresh the ads within the zone with content that's relevant to a specific ID sent to us. Keep in mind, this feature only works with valid context IDs. If you're interested in this feature and haven't set it up yet, feel free to reach out to us for more information on how to get started. #### ** Swift ** ```swift //AaZoneView Class //Sets the current context of the zone and refreshes the ads. func setAdZoneContextId(contextId: String) //Removes this specific zone's context and refreshes to show default ads. func removeAdZoneContext() ``` #### ** Objective-C ** ```objectivec //AaZoneView Class //Sets the current context of the zone and refreshes the ads. [self.aaZoneView setAdZoneContextIdWithContextId:@"context_id"]; //Removes this specific zone's context and refreshes to show default ads. [self.aaZoneView removeAdZoneContext]; ``` ## AaZoneViewSwiftUI There is also a SwiftUI specific ZoneView which can be used in SwiftUI views in a similar fashion. The class is called `AaZoneViewSwiftUI` and it can be utilized as seen in the below example: ```swift @State private var isZoneViewVisible: Bool = false @State private var zoneContextId: String = "organic" AaZoneViewSwiftUI(zoneId: "102110", zoneListener: yourZoneListener, contentListener: yourContentListener, isZoneVisible: $isZoneViewVisible, zoneContextId: $zoneContextId) .frame(width: CGFloat(UIScreen.main.bounds.width), height: 80) .onScrollVisibilityChange { isVisible in isZoneViewVisible = isVisible } ``` We recommend following a similar pattern to the example above by using state-based properties to manage visibility and context logic. This is especially useful if your zone view can scroll or become hidden; otherwise, these elements (isZoneVisible and zoneContextId) are optional. If using the visibility feature, make sure to set the state variable to reflect the default visibility state. For example, if the view is initially hidden and requires scrolling to be seen, set the variable to false at the start. If using the context feature of the zone view, you can clear the context by setting the state variable to an empty string. To set or update the context, simply assign a valid context ID. You can implement your zone and content listeners either through a view model or directly in the view itself. Additionally, the .onScrollVisibilityChange method, introduced in iOS 18, provides the most accurate visibility detection. While we recommend using this feature, you can choose any other method to manage the state-based visibility property. > **Note:** AaZoneViewSwiftUI is only available in Swift. For Objective-C apps, use AaZoneView programmatically as shown above. ## AdContentListener Some ads can insert items directly into your lists if desired. To do this, use the view or controller where the ad interaction is taking place to implement an `AdContentListener`. First, using the correct approach below, implement the content listener: #### ** Swift ** ```swift class ViewController: UIViewController, AdContentListener { // ... } ``` #### ** Objective-C ** ```objectivec // In your .h file @interface ViewController : UIViewController @end ``` Next, you'll need to implement the `onContentAvailable()` method as follows: #### ** Swift ** ```swift func onContentAvailable(zoneId: String, content: AddToListContent) { let items = content.getItems() for item in items { content.itemAcknowledge(item: item) // Acknowledge the item(s) added to the list appendListItem(itemName: item.title) } content.acknowledge() // Acknowledge content was delivered (not necessary if acknowledging per item as above) } ``` #### ** Objective-C ** ```objectivec - (void)onContentAvailableForZone:(NSString *)zoneId content:(AddToListContent *)content { NSArray *items = content.items; for (AddToListItem *item in items) { [content itemAcknowledge:item]; // Acknowledge the item(s) added to the list [self appendListItemWithName:item.title]; } [content acknowledge]; // Acknowledge content was delivered (not necessary if acknowledging per item as above) } ``` The `AddToListContent` object will contain a list of `AddToListItem` and can be accessed using `content.getItems()` or `content.items`. Each item contains several pieces of information about the product in the current ad (product title, image url, etc.). The calls to `itemAcknowledge()` and `acknowledge()` in the code snippet above are necessary to acknowledge that the user clicked on the ad and the product was added to the list. > **Note:** While use of itemAcknowledge() is preferred, the standard acknowledge() method is still valid. This call should be made at the time the product is added to the list, as opposed to when the item is first clicked. > **Note:** There is also a method called onNonContentAction that can be used specifically to listen for action events on ads that do not produce content. An example of this could be external linking ads or pop-up view ads that display information but do not add anything to the user's cart. If the payload was processed, but the product was not added to the list for whatever reason, you can indicate this using one of the following two methods: #### ** Swift ** ```swift content.itemFailed(item, "Reason for not adding product to list") ``` #### ** Objective-C ** ```objectivec [content itemFailed:item message:@"Reason for not adding product to list"]; ``` or #### ** Swift ** ```swift content.failed("Reason for adding NO ITEMS to list") ``` #### ** Objective-C ** ```objectivec [content failed:@"Reason for adding NO ITEMS to list"]; ``` Finally, you'll need to add and remove the listener in the view's lifecycle methods. If you only need a content listener (without a zone view listener), you can pass it alone into `onStart()` and `onStop()`. If you also need zone-level notifications, use `startWithZoneViewListener:adContentListener:` instead as shown in the [AaZoneView](#aazoneview) section above. #### ** Swift ** ```swift override func viewDidLoad() { // ... aaZoneView.onStart(contentListener: self) } override func viewDidDisappear(_ animated: Bool) { // ... aaZoneView.onStop(listener: self) } ``` #### ** Objective-C ** ```objectivec - (void)viewDidLoad { [super viewDidLoad]; // ... [self.aaZoneView startWithAdContentListener:self]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; // ... [self.aaZoneView onStop]; } ``` > **Note:** The above code is implemented when setting up an AaZoneView. You just need to pass in self. > **Note:** If you would also like to listen to ad interactions that do not have items or content to deliver (such as external browser linking ads), there is an optional method you can listen for as well called onNonContentAction(). This method provides the ad zone's ID and the ad's ID itself that you can use for any personal data points. ## AdAdaptedListManager Use the `AdAdaptedListManager` to acknowledge when an item has been added to, crossed off, or removed from a list. Whenever a product is **added to** a list, acknowledge this by calling the `itemAddedToList()` method as follows: #### ** Swift ** ```swift AdAdaptedListManager.itemAddedToList(list: "swift_adapted_list", item: "added_product_name") ``` #### ** Objective-C ** ```objectivec [AdAdaptedListManager itemAddedToList:@"added_product_name" list:@"swift_adapted_list"]; ``` Items may be **crossed off** (purchased / completed) the list by the user. Acknowledge this by calling the `itemCrossedOffList()` method as follows: #### ** Swift ** ```swift AdAdaptedListManager.itemCrossedOffList(list: "swift_adapted_list", item: "testItemCross") ``` #### ** Objective-C ** ```objectivec [AdAdaptedListManager itemCrossedOffList:@"testItemCross" list:@"swift_adapted_list"]; ``` Items may be **removed from** the list by the user. Acknowledge this by calling the `itemDeletedFromList()` method as follows: #### ** Swift ** ```swift AdAdaptedListManager.itemDeletedFromList(list: "swift_adapted_list", item: "testItemDelete") ``` #### ** Objective-C ** ```objectivec [AdAdaptedListManager itemDeletedFromList:@"testItemDelete" list:@"swift_adapted_list"]; ``` > **Note:** All of the methods above do not require a list name and can be called with just the item name instead: AdAdaptedListManager.itemAddedToList(item: "added_product_name") or [AdAdaptedListManager itemAddedToList:@"added_product_name"]. ## SDK Notifications There are occasions when no ads are available to return at the time of your request. However, notifications are available at runtime should you want to check that there are ads to serve. The following table lists the available notification types: | Type | Description | | ---- | ----------- | | Zone | An `AaZoneView` notification that returns whether a _specific zone_ has ads to serve | ### AaSdkSessionListener > **Note:** Removed in version 3.0.0: AaSdkSessionListener (SessionListener in Objective-C) and setSdkSessionListener() no longer exist in the SDK. Use the per-zone onZoneHasAds() callback on ZoneViewListener below to find out whether a zone has ads to serve. ### AaSdkEventListener To listen to all generic ad events you can setup an event listener to trigger for each ad event type. You'll need to implement the listener function in your AppDelegate and then enable it in your SDK initialization chain: #### ** Swift ** ```swift class AppDelegate: UIResponder, UIApplicationDelegate, AaSdkEventListener { func application() { AdAdapted .withAppId(key: "API_KEY") .inEnv(env: AdAdapted.Env.DEV) .setSdkEventListener(listener: self) //hookup the SdkEventListener .start() } //... func onNextAdEvent(zoneId: String, eventType: String) { print("Ad \(eventType) for Zone \(zoneId)") } } ``` #### ** Objective-C ** ```objectivec // In your .h file @interface AppDelegate : UIResponder @end // In your .m file - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [AdAdapted withAppId:@"API_KEY"]; [AdAdapted inEnvironmentProduction:NO]; [AdAdapted setSdkEventListener:self]; [AdAdapted start]; return YES; } - (void)onNextAdEvent:(NSString *)zoneId eventType:(NSString *)eventType { NSLog(@"Ad %@ for Zone %@", eventType, zoneId); } ``` ### ZoneViewListener To listen for **Zone** level notifications you can implement a zone view listener on your view controller class where your ad zones are being created/initialized and pass self to the `onStart()` method for your `AaZoneView` as the listener parameter: #### ** Swift ** ```swift class ViewController:UIViewController, ZoneViewListener { override func viewDidLoad() { aaZoneView.onStart(listener: self, contentListener: self) // Accepts both zone and content listeners at once } // ... func onAdLoaded() { } func onAdLoadFailed() { } func onZoneHasAds(hasAds: Bool) { } } ``` #### ** Objective-C ** ```objectivec // In your .h file @interface ViewController : UIViewController @end // In your .m file - (void)viewDidLoad { [super viewDidLoad]; [self.aaZoneView startWithZoneViewListener:self adContentListener:self]; } - (void)onZoneHasAds:(BOOL)hasAds { NSLog(@"Zone has ads: %d", hasAds); } - (void)onAdLoaded { NSLog(@"Ad loaded"); } - (void)onAdLoadFailed { NSLog(@"Ad load failed"); } ``` The methods `onAdLoaded()` and `onAdLoadFailed()` shown in the snippets above tell you whether an ad that was already fetched was able to display in the zone's web view. `onAdLoadFailed()` means the web view couldn't load the ad's creative, usually because of bad HTML or a problem loading something the creative references. It does not fire when the ad request to AdAdapted fails. These events are rare and these methods should only be used in very specific circumstances. As of version 4.0.0 `onZoneHasAds()` is reported every time the zone gets a response, not just on the first load. If a refresh comes back with no ad available, it reports `false`. If a later refresh comes back with an ad, it reports `true` again. Keep listening for as long as the zone is on screen and collapse or restore its space in your layout to match, so an empty zone doesn't leave a gap in your UI. The callback comes back on the main queue, so it's safe to update your layout right there. Keep in mind that this only covers responses we actually get back. If the ad request itself fails because of a network or server error, the zone renders blank without reporting `false` and without calling `onAdLoadFailed()`. That means `onZoneHasAds()` on its own isn't enough to decide whether to reserve the zone's space. A zone only counts down to its next refresh while it's attached, visible, in the window, and the app is in the foreground. Setting the zone's `isZoneVisible` binding to `false`, calling `setAdZoneVisibility(isViewable: false)`, or removing the zone from the view hierarchy all pause that countdown, so a hidden zone won't refresh and can't report `true` on its own. Once you make it visible again the countdown picks back up, and the zone reports `onZoneHasAds()` with its next response, fetching right away if its refresh time already passed while it was hidden. Bring the zone back based on something in your own app rather than waiting on a callback, since a hidden zone can't send one. > **Note:** If onZoneHasAds() reports false for a given zone ID, no ad is loaded for that response and the zone renders blank. The zone keeps refreshing while it's on screen, so a later response can still report true. ## KeywordInterceptMatcher Once the SDK is initialized, you can use the `KeywordInterceptMatcher` when performing a search operation within your app. First make sure you have enabled this feature in your initialization chain, then call the `match()` function either as the user types in a search term or when the user executes a search after the term is entered. A list of keyword search results in the form of `Suggestion` will be returned by this method that can then be used to display dynamically along with the search results your app displays to the user. The final result of the search may have many items, in which case you would determine how many of them you would like to display. As an example, you may utilize the `KeywordInterceptMatcher` as follows: #### ** Swift ** ```swift func afterTextChanged(suggestion: String) { let suggestionResults = KeywordInterceptMatcher.getInstance().match(constraint: suggestion) } ``` #### ** Objective-C ** ```objectivec - (void)textFieldDidChange:(UITextField *)textField { NSArray *suggestions = [KeywordInterceptMatcher match:textField.text]; } ``` > **Note:** Sorting based on priority is already handled for you by the SDK. The results will be in the correct sort order by default. Also remember to enable the Keyword Intercept functionality as part of your SDK initialization. The final result of suggestions is pre-sorted based on the following criteria: - First, the suggestions are sorted based on which terms "start" with the characters in the provided search string. - Second, they are then sorted by a pre-determined priority for each keyword that satisfied the search. There are two additional methods on the `Suggestion` objects that should be called when using Keyword Intercept. Whenever a suggestion has been displayed, make sure to call `wasPresented()` to acknowledge the keyword intercept item was presented to the user as follows: #### ** Swift ** ```swift suggestion.wasPresented() ``` #### ** Objective-C ** ```objectivec [suggestion wasPresented]; ``` If the user selects the presented suggestion, this action should be acknowledged using the `wasSelected()` method as follows: #### ** Swift ** ```swift suggestion.wasSelected() ``` #### ** Objective-C ** ```objectivec [suggestion wasSelected]; ``` > **Note:** Making sure to call these methods when a keyword is displayed/selected will ensure AdAdapted can provide you with the most accurate reporting results. ## AaSdkAdditContentListener Implementing out-of-app Add-It allows ads served in other mobile applications or deeplinks to direct the user back into the list app to add the product to their shopping list. Functionally, you'll just need to set up a content listener to watch for outside content payloads and handle them appropriately. To initialize this functionality, you need to implement the content listener in your AppDelegate and then enable it in your SDK initialization chain to set up the handling for the payload: #### ** Swift ** ```swift class AppDelegate: UIResponder, UIApplicationDelegate, AaSdkAdditContentListener { func application() { AdAdapted .withAppId(key: "API_KEY") .inEnv(env: AdAdapted.Env.DEV) .enablePayloads(value: true) //enable payload delivery .setSdkAdditContentListener(listener: self) //hookup the AaSdkAdditContentListener .start() } //... func onContentAvailable(content: AddToListContent) { let listItems = content.getItems() let itemTitle = listItems.first?.title ?? "" // Handle incoming content content.acknowledge() // Acknowledge the item(s) added to the list print("\(itemTitle) added to Default List") } } ``` #### ** Objective-C ** ```objectivec // In your .h file @interface AppDelegate : UIResponder @end // In your .m file - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [AdAdapted withAppId:@"API_KEY"]; [AdAdapted inEnvironmentProduction:NO]; [AdAdapted enablePayloads:YES]; [AdAdapted setSdkAdditContentListener:self]; [AdAdapted start]; return YES; } - (void)onContentAvailable:(AddToListContent *)content { NSArray *listItems = content.items; NSString *itemTitle = listItems.firstObject.title ?: @""; // Handle incoming content [content acknowledge]; // Acknowledge the item(s) added to the list NSLog(@"%@ added to Default List", itemTitle); } ``` **Other Delivery Methods for Out-of-App Payload Ads** When beginning development on out-of-app payload ads, please work with our team to confirm that a payload and a test link has been prepared for your app. Once you are ready to begin the testing phase, this app-specific link can be used to schedule a payload. ![Out-Of-App Ad Payload Test](../images/out-of-app-ad-payload-test.png) --- ### Upgrade Guide # Upgrading to the Swift Package iOS SDK **Quick Links:** - [Upgrading from CocoaPods Swift SDK](#upgrading-from-cocoapods-swift-sdk) - [Upgrading from Objective-C SDK](#upgrading-from-objective-c-sdk) --- # Upgrading from CocoaPods Swift SDK If you're coming from the older CocoaPods-based Swift SDK, follow this section to migrate to the new Swift Package SDK. ## Install and Cleanup First, add the Swift package (`adadapted-swift-sdk`) via the package manager to your project. Next, we recommend removing the old iOS CocoaPod and letting the unresolved code be your guide to making sure no integration points are missed along the way. ## Startup Initialization Initialization is still done in the AppDelegate class but it uses a chained call to enable/disable the features you want. It also leverages new listeners that the delegate must inherit from if you want to listen for these events. ```swift @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate, AaSdkEventListener, AaSdkAdditContentListener { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { AdAdapted .withAppId(key: "API_KEY") // Replace the API_KEY with the key provided to you .inEnv(env: AdAdapted.Env.DEV) // When your app is ready for release, change this flag to PROD .enableKeywordIntercept(value: true) //OPTIONAL: Enable keyword intercept if you plan on using this feature .enablePayloads(value: true) //OPTIONAL: Enable payload feature if needed .enableDebugLogging() //OPTIONAL: Enable for extra debug information .setSdkEventListener(listener: self) //OPTIONAL: Enable to listen for generic events .setSdkAdditContentListener(listener: self) //OPTIONAL: Enable to listen for content events .start() return true } } ``` ## Ad Zones Ad zones are now named `AaZoneView` and are used similarly but need to be initialized first and then started: ```swift override func viewDidLoad() { aaZoneView.initialize(zoneId: "your_zone_id") aaZoneView.setAdZoneVisibility(isViewable: false) //optional if the zone is utilizing visibility changes aaZoneView.onStart(listener: self, contentListener: self) } ``` They also need to be stopped when the view changes to ensure the zone's listeners are properly destroyed: ```swift override func viewDidDisappear(_ animated: Bool) { aaZoneView.onStop(listener: self) } ``` The views that house the zones also have new listeners to inherit from for events: ```swift class ViewController: ZoneViewListener, AdContentListener ``` `AdContentListener` specifically is used to perform the adding of items: ```swift func onContentAvailable(zoneId: String, content: AddToListContent) { let items = content.getItems() for item in items { content.itemAcknowledge(item: item) // Acknowledge the item(s) added to the list appendListItem(itemName: item.title) } content.acknowledge() // Acknowledge content was delivered (not necessary if acknowledging per item as above) } ``` ## Reporting Items Added, Removed, and Crossed Off Reporting item changes is now handled in a simple ListManager: ```swift AdAdaptedListManager.itemAddedToList(item: "added_product_name") AdAdaptedListManager.itemAddedToList(item: "testItemAdd", list: "swift_adapted_list") AdAdaptedListManager.itemCrossedOffList(item: "testItemCross") AdAdaptedListManager.itemDeletedFromList(item: "testItemDelete") ``` ## Event Listeners Events are now easier to listen to by simply utilizing which ones you want: ### AaSdkSessionListener Removed in version **3.0.0**: `AaSdkSessionListener` (`SessionListener` in Objective-C) and `setSdkSessionListener()` no longer exist. If you listened for the old global "has ads to serve" notification, use the per-zone `onZoneHasAds()` callback on `ZoneViewListener` instead. ### AaSdkEventListener To listen to all generic ad events you can setup the `AaSdkEventListener` to trigger for each ad event type. You'll need to implement the listener function in your AppDelegate and then enable it in your SDK initialization chain: ```swift class AppDelegate: UIResponder, UIApplicationDelegate, AaSdkEventListener { func application() { AdAdapted .withAppId(key: "API_KEY") .inEnv(env: AdAdapted.Env.DEV) .setSdkEventListener(listener: self) //hookup the SdkEventListener .start() } //... func onNextAdEvent(zoneId: String, eventType: String) { print("Ad \(eventType) for Zone \(zoneId)") } } ``` ### ZoneViewListener To listen for **Zone** level notifications you can implement `ZoneViewListener` on your view controller class where your ad zones are being created/initialized and pass self to the `onStart()` method for your `AaZoneView` as the listener parameter: ```swift class ViewController:UIViewController, ZoneViewListener { override func viewDidLoad() { aaZoneView.onStart(listener: self, contentListener: self) // Accepts both zone and content listeners at once } // ... func onAdLoaded() { } func onAdLoadFailed() { } func onZoneHasAds(hasAds: Bool) { } } ``` ## Keyword Intercept Keyword Intercept usage is also more straightforward: ```swift func afterTextChanged(suggestion: String) { let suggestionResults = KeywordInterceptMatcher.getInstance().match(constraint: suggestion) } ``` The presented and selected calls are now directly on the suggestion objects: ```swift suggestion.wasPresented() suggestion.wasSelected() ``` > **Note:** Make sure you have enabled this feature in your initialization chain [.enableKeywordIntercept(value: true)] before accessing it. # And that's (mostly) it! For any other features, functionality, or questions please see the full documentation: [iOS SPM Documentation](docs/ios-usage.md) --- # Upgrading from Objective-C SDK If you're coming from an Objective-C app using the older CocoaPods SDK (`AASwiftSDK`) or the legacy Objective-C SDK, follow this section to migrate to the new Swift Package SDK using the Objective-C compatibility wrappers. The new Swift Package SDK includes built-in Objective-C wrappers, so you can integrate it into your existing Objective-C app without rewriting in Swift. ## Install and Cleanup 1. Add the Swift package (`adadapted-swift-sdk`) via Swift Package Manager to your Xcode project. 2. Remove the old CocoaPod (`AASwiftSDK` or `AdAdaptedSDK`) from your Podfile and run `pod install`. 3. Replace all imports of the old SDK: ```objectivec // Old import @import AASwiftSDK; // New import @import adadapted_swift_sdk; ``` ## Class Name Changes The Objective-C wrapper classes use a new naming convention. Here are the key changes: | Old SDK | New Swift Package (ObjC Wrappers) | | ------- | --------------------------------- | | `AASDKObserver` | `AaSdkEventListener` (the global "has ads to serve" callback is gone; use `ZoneViewListener` per zone) | | `AASDKContentDelegate` | `AdContentListener` | | `AdAdaptedZoneView` | `AaZoneView` | | `AAZoneViewOwner` | `ZoneViewListener` | | `AdContent` | `AddToListContent` | | `AADetailedListItem` | `AddToListItem` | | N/A | `AdAdaptedListManager` | | N/A | `KeywordInterceptMatcher` | ## Startup Initialization Initialization is now done via a series of static calls on `AdAdapted` rather than a single `startSessionWithAppID:` call with an options dictionary. Your AppDelegate should conform to the new listener protocols: ```objectivec // AppDelegate.h @import adadapted_swift_sdk; @interface AppDelegate : UIResponder @end ``` ```objectivec // AppDelegate.m @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [AdAdapted withAppId:@"API_KEY"]; // Replace with your API key [AdAdapted inEnvironmentProduction:NO]; // Change to YES for release [AdAdapted enableKeywordIntercept:YES]; // OPTIONAL [AdAdapted enablePayloads:YES]; // OPTIONAL [AdAdapted enableDebugLogging]; // OPTIONAL [AdAdapted setSdkEventListener:self]; // OPTIONAL [AdAdapted setSdkAdditContentListener:self]; // Necessary for Add To List [AdAdapted start]; return YES; } @end ``` > **Note:** Replace the API_KEY placeholder string with the key provided to you by AdAdapted. When your app is ready for release, change inEnvironmentProduction:NO to inEnvironmentProduction:YES. ## Ad Zones Ad zones now use `AaZoneView` instead of `AdAdaptedZoneView`. The zone view can be created programmatically and no longer requires a storyboard or Interface Builder setup with `setZoneOwner:`. Instead, you initialize it with a zone ID and start it with listeners: ```objectivec // In your .h file @interface ViewController : UIViewController @property (nonatomic, strong) AaZoneView *zoneView; @end ``` ```objectivec // In your .m file - (void)viewDidLoad { [super viewDidLoad]; self.zoneView = [[AaZoneView alloc] initWithFrame:CGRectZero]; [self.view addSubview:self.zoneView]; [self.zoneView initializeWithZoneId:@"your_zone_id"]; [self.zoneView setAdZoneVisibility:NO]; // Optional: if the zone requires scrolling to be seen [self.zoneView startWithZoneViewListener:self adContentListener:self]; } ``` Make sure to stop the zone view when the view disappears: ```objectivec - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; [self.zoneView onStop]; } ``` The zone view listener methods are now part of the `ZoneViewListener` protocol instead of `AAZoneViewOwner`: ```objectivec #pragma mark - ZoneViewListener - (void)onZoneHasAds:(BOOL)hasAds { NSLog(@"Zone has ads: %d", hasAds); } - (void)onAdLoaded { NSLog(@"Ad loaded"); } - (void)onAdLoadFailed { NSLog(@"Ad load failed"); } ``` ## Add To List Content Content handling has changed from notification-based (`AASDKContentDelegate` / `aaContentNotification:`) to a direct listener protocol. Your view controller now conforms to `AdContentListener`: ```objectivec #pragma mark - AdContentListener - (void)onContentAvailableForZone:(NSString *)zoneId content:(AddToListContent *)content { for (AddToListItem *item in content.items) { [content itemAcknowledge:item]; // Acknowledge the item(s) added to the list [self addItemToList:item.title]; } [content acknowledge]; // Acknowledge content was delivered } // Optional: listen for non-content ad actions (e.g., link ads) - (void)onNonContentAction:(NSString *)zoneId adId:(NSString *)adId { NSLog(@"Non-content action for zone: %@, ad: %@", zoneId, adId); } ``` > **Note:** While use of itemAcknowledge: is preferred, the standard acknowledge method is still valid. This call should be made at the time the product is added to the list, as opposed to when the item is first clicked. ## Reporting Items Added, Removed, and Crossed Off Item tracking is now handled through `AdAdaptedListManager` instead of `AASDK reportItem:`: ```objectivec // Old SDK [AASDK reportItem:@"product_name" addedToList:@"list_name"]; [AASDK reportItem:@"product_name" crossedOffList:@"list_name"]; [AASDK reportItem:@"product_name" deletedFromList:@"list_name"]; // New SDK [AdAdaptedListManager itemAddedToList:@"product_name" list:@"list_name"]; [AdAdaptedListManager itemCrossedOffList:@"product_name" list:@"list_name"]; [AdAdaptedListManager itemDeletedFromList:@"product_name" list:@"list_name"]; ``` > **Note:** All of the methods above also accept just the item name without a list name: [AdAdaptedListManager itemAddedToList:@"product_name"]. ## Event Listeners The old SDK used `AASDKObserver` with notification-based callbacks. The new SDK uses dedicated listener protocols: ### SessionListener (removed) The old `aaSDKInitComplete:` notification has no direct replacement. `SessionListener` and `[AdAdapted setSdkSessionListener:]` were removed in version **3.0.0** — check for ads per zone with the `onZoneHasAds:` callback on `ZoneViewListener` instead: ```objectivec - (void)onZoneHasAds:(BOOL)hasAds { NSLog(@"Zone has ads to serve: %d", hasAds); } ``` ### AaSdkEventListener ```objectivec - (void)onNextAdEvent:(NSString *)zoneId eventType:(NSString *)eventType { NSLog(@"Ad %@ for Zone %@", eventType, zoneId); } ``` ## Keyword Intercept Keyword Intercept no longer uses `AASDK keywordInterceptFor:` with an `NSDictionary` response. Instead, use `KeywordInterceptMatcher` which returns an array of `Suggestion` objects: ```objectivec // Old SDK NSDictionary *results = [AASDK keywordInterceptFor:searchText]; NSString *suggestionName = results[AASDK.KEY_KI_REPLACEMENT_TEXT]; [AASDK keywordInterceptPresented]; [AASDK keywordInterceptSelected]; // New SDK NSArray *suggestions = [KeywordInterceptMatcher match:searchText]; for (Suggestion *suggestion in suggestions) { NSLog(@"Suggestion: %@", suggestion.name); [suggestion wasPresented]; } // When user selects a suggestion (e.g., in your table view didSelectRowAtIndexPath:): Suggestion *selectedSuggestion = suggestions[indexPath.row]; [selectedSuggestion wasSelected]; [KeywordInterceptMatcher suggestionWasSelected:selectedSuggestion.name]; ``` > **Note:** Make sure you have enabled Keyword Intercept during initialization with [AdAdapted enableKeywordIntercept:YES] before using it. ## Out-of-App Payload (Addit Content) Out-of-app payload handling has moved from `aaPayloadNotification:` to the `AaSdkAdditContentListener` protocol set during initialization: ```objectivec // Set during SDK init [AdAdapted enablePayloads:YES]; [AdAdapted setSdkAdditContentListener:self]; // Implement the listener - (void)onContentAvailable:(AddToListContent *)content { for (AddToListItem *item in content.items) { NSLog(@"Payload item: %@", item.title); // Handle incoming content } [content acknowledge]; } ``` ## Universal Links Universal link handling has been simplified: ```objectivec // Old SDK [AASDK linkContentParser:userActivity]; // New SDK [AdAdaptedLinkHandler parseUniversalLink:userActivity.webpageURL.absoluteString]; ``` # And that's (mostly) it! For any other features, functionality, or questions please see the full documentation: [iOS SPM Classes](docs/ios-classes.md) --- ## Swift / Obj-C SDK (CocoaPods) ### Getting Started ### Features - Ad zone creation - In-app ad display popup/browser - In-list add suggestion based on available keywords - Event tracking enabling reporting - Supported ad types: - Add to list (reports a product to add to a list) - Add to list circular (opens in-app within a popup view and displays a multi-item "add to list" circular) - Add to list from outside the app (Deeplinks etc.) - Standard external link (opens in external browser) - Popup external link (opens in-app within popup view) - Context based ads determined by a recipe or context ID > **Note:** If you don't need access to objective C classes please use the latest swift package library found here: Swift Package SDK for iOS ### CocoaPod Installation The SDK is primarily available via CocoaPods. If you currently don't use the CocoaPods dependency manager, you can install it running the following command in your terminal: ```bash gem install cocoapods ``` Once CocoaPods has finished installing, change the current directory to your project root directory and run the following command in your terminal: ```bash pod init ``` Next, integrate the SDK framework by opening your `Podfile` and add the following: ```ruby target 'YOUR_PROJECT' do use_frameworks! pod 'AASwiftSDK' end ``` Lastly, run the following command in your terminal to finalize the installation of the SDK: ```bash pod install ``` Once this command finishes running, the output will indicate that a `[project_name].xcworkspace` file has been created. You'll need to close Xcode and open your project using this file from now on. ## AASDK Import Simply add the sdk to your code via import statement to gain access: #### ** Swift ** ```swift import AASwiftSDK ``` #### ** Objective-C ** ```objc @import AASwiftSDK; ``` > **Note:** If you are using Objective-C, this import method should auto-generate the headers for you. Your application should now build successfully and you can begin using the AdAdapted Swift iOS SDK. ## License [AdAdapted Platform License](docs/license.md) --- ### Usage ## Initialization > **Note:** Make sure to import the AdAdapted SDK into any file you are using the SDK within. ``` import AASwiftSDK ``` To initialize the AdAdapted Swift iOS SDK, you will first need to call `startSession` from your **AppDelegate** file (or a View Controller). This method takes the following arguments: | Argument Name | Required? | Description | | ------------- | --------- | ----------- | | `api_key` | Yes |A unique string provided to you from AdAdapted | | `observer` | No | An optional reference or id to a delegate conforming to the AASDKObserver protocol | | `options` | No | An optional dictionary of configuration options | #### ** Swift ** ```swift @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { let options = [ AASDK.OPTION_TEST_MODE:true, // When your app is ready for release, change or remove this flag AASDK.OPTION_KEYWORD_INTERCEPT:true, //Enable keyword intercept functionality AASDK.OPTION_EXTERNAL_PAYLOADS:true, //Enable external payload functionality ] as [String : Any] // Replace the API_KEY with the key provided to you AASDK.startSession(withAppID: "API_KEY", registerListenersFor: self, options: options) return true } } ``` #### ** Objective-C ** ```objc // In your .m file @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { NSDictionary *options = @{ AASDK.OPTION_TEST_MODE:@YES, // When your app is ready for release, change or remove this flag AASDK.OPTION_KEYWORD_INTERCEPT:@YES, //Enable keyword intercept functionality AASDK.OPTION_EXTERNAL_PAYLOADS:@YES //Enable external payload functionality }; // Replace the API_KEY with the key provided to you [AASDK startSessionWithAppID:@"API_KEY" registerListenersFor:self options:options]; return YES; } @end ``` > **Note:** Please note as of version 1.1.X, it is now necessary to init both Keyword Intercept and External Payloads manually if you would like to use these features. This can be done by turning on the corresponding option calls mentioned in the code block above. You will also extend the AppDelegate to handle the `AASDKObserver` and define a couple of callback functions as follows: #### ** Swift ** ```swift class AppDelegate: AASDKObserver { func aaSDKInitComplete(_ notification: Notification) { print("init complete") } func aaSDKError(_ error: Notification) { print("error \(error.debugDescription)") } } ``` #### ** Objective-C ** ```objc // In your .h file @interface AppDelegate : UIResponder @property (strong, nonatomic) UIWindow *window; @end // In your .m file - (void)aaSDKInitComplete:(nonnull NSNotification *)notification { NSLog(@"%@", notification); } - (void)aaSDKError:(nonnull NSNotification *)error { NSLog(@"%@", error); } ``` ## Ad Zones A zone is a space in the application in which an ad may be displayed. Zones have a numeric ID, which is provided by AdAdapted and exposed to the SDK in the code or the interface builder. Once set up in the app, the AdAdapted platform can serve ads to the zone via the SDK. > **Note:** The following example uses the interface builder. First, open the **Storyboard** and add a **UIView** element. On the Identity Inspector tab, use the Class dropdown set the class to **AdAdaptedZoneView** and add a User Defined Runtime Attribute with the following values: ``` Key Path = "zoneId" Type = String Value = YOUR_ZONE_ID ``` ![User Defined Runtime Attribute](../images/swift-add-zone.png) Lastly, **ctrl + drag** your zone view to the **ViewController** and create an outlet property (called _adZone_ below), then set the **AAZoneViewOwner** delegate to your view controller as follows: #### ** Swift ** ```swift class ViewController: UIViewController { @IBOutlet var adZone: AdAdaptedZoneView! override func viewDidLoad() { super.viewDidLoad() adZone.setZoneOwner(self) } } extension ViewController: AAZoneViewOwner { func viewControllerForPresentingModalView() -> UIViewController! { return self } } ``` #### ** Objective-C ** ```objc // In your .h file @interface ViewController : UIViewController // In your .m file @property (weak, nonatomic) IBOutlet AdAdaptedZoneView *adZone; - (void)viewDidLoad { [super viewDidLoad]; [_adZone setZoneOwner:self]; } - (UIViewController *)viewControllerForPresentingModalView { return self; } ``` You can now compile and run your app and an ad will be displayed. ## Context Based Ads (Recipes) If you're using our context/recipe implementation, you can use the following methods on the `AdAdaptedZoneView` to enable a context mode for ad zones. This will refresh the ads within the zone with content that's relevant to a specific ID sent to us. Keep in mind, this feature only works with valid context IDs. If you're interested in this feature and haven't set it up yet, feel free to reach out to us for more information on how to get started. ```swift //AdAdaptedZoneView Class //Sets the current context of the zone and refreshes the ads. func setAdZoneContext(contextID: String) //Removes this specific zone's context and refreshes to show default ads. func removeAdZoneContext() //Clears the context of all zones and refreshes them to show standard ads. func clearAdZonesContext() ``` ## Add To List Some ads have the ability to pass product information back to the app. The best way to use this information is to add the product(s) to a list. To do this, have your **ViewController** conform to the AASDKContentDelegate protocol and set it to the view controller as follows: #### ** Swift ** ```swift override func viewDidLoad() { super.viewDidLoad() adZone.setZoneOwner(self) AASDK.registerContentListeners(for: self) } extension ViewController: AASDKContentDelegate { func aaContentNotification(_ notification: Notification) { print("In-app content available") guard let userinfo = notification.userInfo else { return } guard let adContent = userinfo[AASDK.KEY_AD_CONTENT] as? AdContent else { return } for item in adContent.detailedListItems { print("AADetailedListItem: ",item.productTitle) // Take some action on item } // Acknowledge the items were added to the list adContent.acknowledge() } } ``` #### ** Objective-C ** ```objc // In your .h file @interface ViewController : UIViewController // In your .m file - (void)viewDidLoad { [super viewDidLoad]; [_adZone setZoneOwner:self]; [AASDK registerContentListenersFor:self]; } - (void)aaContentNotification:(NSNotification*)notification { NSLog(@"In-app content available"); NSString* zoneId = [[notification userInfo] objectForKey:AASDK.KEY_ZONE_ID]; AdContent *adContent = [[notification userInfo] objectForKey:AASDK.KEY_AD_CONTENT]; for (AADetailedListItem *item in adContent.detailedListItems) { NSLog(@"AADetailedListItem: %@", item.productTitle); // Take some action on item } // Acknowledge the items were added to the list [adContent acknowledge]; } ``` In the above code snippet, `aaContentNotification()` will be triggered whenever the ad is tapped. A notification object is passed which contains a payload with a list of products associated with the ad. The notification contains an `AdContent` object with a list of `AADetailedListItem` objects. Each `AADetailedListItem` object contains information about the product(s) in the ad (product title, image url, etc.). > **Note:** Please reference the AADetailedListItem Data Model section for more information on the AADetailedListItem payload. After adding items to a list, call `adContent.acknowledge()` to notify the SDK that items have been added to a list. This is necessary to acknowledge that the user actually clicked on the ad, otherwise, your app may report an incorrect and lower number of clicks than the true amount. > **Note:** If you would also like to listen to ad interactions that do not have items or content to deliver (such as external browser linking ads), there is an optional method you can listen for as well called aaNonContentNotification(). This method provides the ad zone's ID and the ad's ID itself that you can use for any personal data points. --- **Add To List on a separate View Controller** In some applications, the ad exists in a View Controller separate from where ads are actually added to the list (from a product view, for example). In that case, pass the `AdContent` object along to the destination View Controller and call acknowledge() when items are actually added: #### ** Swift ** ```swift func aaContentNotification(_ notification: Notification) { guard let userinfo = notification.userInfo else { return } guard let adContent = userinfo[AASDK.KEY_AD_CONTENT] as? AdContent else { return } // Segue and pass content along for later acknowledgment performSegue(withIdentifier: "showProductPage", sender: adContent) } ``` #### ** Objective-C ** ```objc - (void)aaContentNotification:(NSNotification*)notification { NSString* zoneId = [[notification userInfo] objectForKey:AASDK.KEY_ZONE_ID]; AdContent *adContent = [[notification userInfo] objectForKey:AASDK.KEY_AD_CONTENT]; // Segue and pass content along for later acknowledgment [self performSegueWithIdentifier:@"segueToProductPage" sender:adContent]; } ``` ## List Manager To improve the user experience and enhance overall advertising performance, AdAdapted provides the List Manager as a part of our integration with new app partners. There are three primary methods that can be easily implemented for this purpose as a part of the normal list lifecycle of the app. Whenever a product is **added to a list**, acknowledge this by calling the `reportItem()` method, or when adding multiple products to a list, the `reportItems()` method. These methods also allow you to account for other list events such as when items are **crossed off** (purchased / completed) or **removed** (no longer wish to purchase / not completed): #### ** Swift ** ```swift // Item added to list AASDK.reportItem("added_product_name", addedToList: "list_name") AASDK.reportItems(["first_product_name","second_product_name","etc."], addedToList: "list_name") // Item crossed off list AASDK.reportItem("crossed_off_product_name", crossedOffList: "list_name") AASDK.reportItems(["first_product_name","second_product_name","etc."], crossedOffList: "list_name") // Item removed from list AASDK.reportItem("deleted_product_name", deletedFromList: "list_name") AASDK.reportItems(["first_product_name","second_product_name","etc."], deletedFromList: "list_name") ``` #### ** Objective-C ** ```objc // Item added to list [AASDK reportItem: @"added_product_name" addedToList: @"list_name"]; [AASDK reportItems: @[@"first_product_name",@"second_product_name",@"etc."] addedToList: @"list_name"]; // Item crossed off list [AASDK reportItem: @"crossed_off_product_name" crossedOffList: @"list_name"]; [AASDK reportItems: @[@"first_product_name",@"second_product_name",@"etc."] crossedOffList: @"list_name"]; // Item removed from list [AASDK reportItem: @"deleted_product_name" deletedFromList: @"list_name"]; [AASDK reportItems: @[@"first_product_name",@"second_product_name",@"etc."] deletedFromList: @"list_name"]; ``` ## Ad Availability There may be occasions when there are no ads available to return at the time of your request. However, there are methods available at runtime should you want to check that there are ads to serve. These methods can be used alone or in combination > **Note:** The example below uses these methods in combination. When the SDK is initialized, `aaSDKInitComplete()` will trigger indicating the SDK loaded properly. You can then check all zones (or a specific zone) to see if any ads are available as follows: #### ** Swift ** ```swift func aaSDKInitComplete(_ notification: Notification) { print("AA init complete") // Get an array of zone IDs for application let ids = AASDK.availableZoneIDs() // Check for valid zone IDs if (ids.isEmpty) { print("No ad zones available") // Don't try to load any ads } else { // Check valid zone IDs for available ads for id in ids { if (AASDK.zoneAvailable(id as? String)) { print("Zone \(id) is available") // Try to load ads... } } } } ``` #### ** Objective-C ** ```objc - (void)aaSDKInitComplete:(nonnull NSNotification *)notification { NSLog(@"%@", notification); // Get an array of zone IDs for application NSArray *ids = [AASDK availableZoneIDs]; // Check for valid zone IDs if (!ids || !ids.count) { NSLog(@"No ad zones available"); // Don't try to load any ads } else { // Check valid zone IDs for available ads for (NSString *zoneId in ids) { if ([AASDK zoneAvailable: zoneId]) { NSLog(@"Zone #%@ has ads available to load", zoneId); // Try to load ads... } } } } ``` The method `AASDK.zoneAvailable()` can also be used independently to check a specific zone ID for ads. Additionally, within the **ViewController**, `zoneViewDidLoadZone()` and `zoneViewDidFailToLoadZone()` are callbacks which are available to indicate whether the zone you are using loaded correctly or failed to load (usually due to network error, bad HTML, etc.). These events are rare and these methods should only be used in very specific circumstances. > **Note:** If AASDK.zoneAvailable() returns false for a given zone ID, ads will not attempt to load. #### ** Swift ** ```swift func zoneViewDidLoadZone(_ view: AAZoneView) { print("loaded zone: \(view.zoneId )") } func zoneViewDidFail(toLoadZone view: AAZoneView) { print("failed to load zone: \(view.zoneId)") } ``` #### ** Objective-C ** ```objc - (void)zoneViewDidLoadZone:(AAZoneView *)view { NSLog(@"loaded zone: %@", view.zoneId); } - (void)zoneViewDidFailToLoadZone:(AAZoneView *)view { NSLog(@"failed to load zone: %@", view.zoneId); } ``` ## Keyword Intercept As a brief summary, the Keyword Intercept integration works as outlined below. Note that the primary driver of this functionality is the `keywordInterceptFor()` method. Beyond that, specific integration steps vary from app to app based on the view used to display suggestions and other configuration settings. **How It Works** When the SDK is initialized, keywords are delivered to the app in a one-time event. As the user types into the text field, the `keywordInterceptFor()` method can be used to match against the ad keywords within the SDK and return a response as an `NSDictionary`. The dictionary contains a number of data points, but most relevant is the name of the product suggested. At that point, the product can be displayed to the user who can then select the suggested item and add it to their list as normal. **Technical Implementation** To start using Keyword Intercept functionality, you will first need to make an update to the options dictionary used in the `startSessionWithAppID()` method within your **AppDelegate** file as follows: #### ** Swift ** ```swift func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { let options = [ // When your app is ready for release, change or remove this flag AASDK.OPTION_TEST_MODE:true, // Must be active to use Keyword Intercept AASDK.OPTION_KEYWORD_INTERCEPT:true ] as [String : Any] // ... } ``` #### ** Objective-C ** ```objc - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { NSDictionary *options = @{ // When your app is ready for release, change or remove this flag AASDK.OPTION_TEST_MODE:@YES, // Must be active to use Keyword Intercept AASDK.OPTION_KEYWORD_INTERCEPT:@YES }; // ... } ``` The above code will tell the SDK to initialize Keyword Intercept at launch. You can also include the `aaSDKKeywordInterceptInitComplete()` callback method, which indicates that initialization was successful. This method can be implemented as follows: #### ** Swift ** ```swift func aaSDKKeywordInterceptInitComplete(_ notification : Notification) { print(notification) } ``` #### ** Objective-C ** ```objc - (void)aaSDKKeywordInterceptInitComplete:(NSNotification *)notification { NSLog(@"%@", notification); } ``` Next, you'll need to set up the `UITextField` widget for handling keywords. To do so, you will first need to listen for text input, then check that input against any sponsored keywords. From the **Storyboard**, **ctrl + drag** your text field widget to the **View Controller** to create a connection. Specify the connection as **Action**, the type as **UITextField**, the event as **Editing Changed**, and give it an appropriate name as follows: ![UITextField Setup](../images/ios-textfield-config.png) On user input, you should attempt to match the string against any sponsored keywords using the `keywordInterceptFor()` method and check for a valid `NSDictionary` object in the response. To display the suggestions, use the `handleData()` method as follows: #### ** Swift ** ```swift @IBAction func textFieldChanged(_ sender: UITextField) { if let results = AASDK.keywordIntercept(for: sender.text!)! as NSDictionary { print("Keyword intercept suggestion available") handleData(results) } } ``` #### ** Objective-C ** ```objc - (IBAction)textFieldChanged:(UITextField *)sender { NSString *entry = sender.text; NSDictionary *results = [AASDK keywordInterceptFor: entry]; if (results) { NSLog(@"Keyword intercept suggestion available"); [self handleData:results]; } } ``` The `handleData()` function in the code above is specific to the app UI. It’s primary use is to look at the set of keyword results passed in, access the name of the sponsored product(s), and display the suggested product to the user. As an example, accessing the data within `handleData()` could be done as follows: #### ** Swift ** ```swift let suggestionName = results[AASDK.KEY_KI_REPLACEMENT_TEXT] as? String print("Suggestion item name: ", suggestionName) ``` #### ** Objective-C ** ```objc NSString *suggestionName = results[AASDK.KEY_KI_REPLACEMENT_TEXT]; NSLog(@"Suggestion item name: %@", suggestionName); ``` There are two final methods that should be in place when using Keyword Intercept. First, whenever a suggestion has been displayed, make sure to call `keywordInterceptPresented()` to acknowledge the keyword was displayed to the user as follows: #### ** Swift ** ```swift AASDK.keywordInterceptPresented() ``` #### ** Objective-C ** ```objc [AASDK keywordInterceptPresented]; ``` Second, if the user then selects the suggestion, call `keywordInterceptSelected()` to acknowledge the keyword was selected by the user as follows: #### ** Swift ** ```swift AASDK.keywordInterceptSelected() ``` #### ** Objective-C ** ```objc [AASDK keywordInterceptSelected]; ``` Once the above steps are completed, you will be up and running with Keyword Intercept. ## Out of App Add-It Implementing out-of-app Add-It capability allows ads served in other mobile applications to direct the user back into the list app to add the product to their shopping list. Functionally, you'll just need to set up a listener to watch for outside content payloads and handle them appropriately. **Technical Implementation** To configure Add-It functionality, you will simply need to add the following method call to your **View Controller**: #### ** Swift ** ```swift func aaPayloadNotification(_ notification: Notification) { print("Out-of-app content available") guard let userinfo = notification.userInfo else { return } guard let adPayload = userinfo[AASDK.KEY_CONTENT_PAYLOADS] as? [AAContentPayload] else { return } for payload in adPayload { for item in payload.detailedListItems { print("AADetailedListItem: ", item.productTitle) // Handle incoming content } payload.acknowledge() } } ``` #### ** Objective-C ** ```objc - (void)aaPayloadNotification:(NSNotification*)notification { NSLog(@"Out-of-app content available") NSArray *adPayload = [notification.userInfo objectForKey:AASDK.KEY_CONTENT_PAYLOADS]; for (AAContentPayload* payload in adPayload) { for (AADetailedListItem *item in payload.detailedListItems) { NSLog(@"AADetailedListItem: %@", item.productTitle); // Handle incoming content } [payload acknowledge] } } ``` **Handle Incoming Content** In the initialization above, the `aaPayloadNotification()` method handles incoming content from the AdAdapted SDK. The `adPayload` object contains any payloads ready for delivery to the user, which when unpacked returns a list of 1 or more **AADetailedListItem** objects. Processing the `AADetailedListItem` product(s) can be done in similar fashion to an in-app click. **Handle iOS universal links** It is recommended that you add the following lines of code to your **App Delegate** file to handle universal links. Similar to a deeplink, [Universal Links](https://developer.apple.com/documentation/uikit/inter-process_communication/allowing_apps_and_websites_to_link_to_your_content/enabling_universal_links) are used in iOS to open the app directly following an out-of-app ad click. Control is passed to the app using the iOS `application:continueUserActivity:restorationHandler` method, and handling of the `aaPayload` object is done just as before via the `aaPayloadNotification()` method. > **Note:** You will need to work with the AdAdapted team to confirm proper configuration settings on our end prior to using this feature. #### ** Swift ** ```swift func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool { AASDK.linkContentParser(userActivity) return true } ``` #### ** Objective-C ** ```objc - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray> * _Nullable))restorationHandler { [AASDK linkContentParser:userActivity]; return true; } ``` > **Note:** If you are currently using iOS 13 or later, there is a change to how universal links are handled resulting in the above method needing to be placed in your SceneDelegate rather than your AppDelegate. **Handle iOS 13+ universal links** In your **SceneDelegate** add this method and call the `linkContentParser()` method here instead: #### ** Swift ** ```swift func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { AASDK.linkContentParser(userActivity) } ``` #### ** Objective-C ** ```objc - (void)scene:(UIScene *)scene continueUserActivity:(nonnull NSUserActivity *)userActivity { [AASDK linkContentParser:userActivity]; } ``` In addition to the above code, you will have to update your app build settings in Xcode for universal links to work properly. Simply go to the `Capabilities` tab in the app settings view, and under the `Associated Domains` section add an entry for **applinks:ul.adadapted.com** as shown below: ![Associated Domains](../images/associated-domains.png) ## Caveats **Optional debug logging** Enabling optional debug logging can assist with troubleshooting or otherwise any implementation errors being encountered. Simply add a new listener in the initialization of the SDK (`registerDebugListeners`) and inherit from an additional observer (`AASDKDebugObserver`). #### ** Swift ** ```swift @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { // ... AASDK.startSession(withAppID: "API_KEY", registerListenersFor: self, options: options) // Setup for optional debug listener must be AFTER the startSession call above. let debug = [AASDK.DEBUG_ALL] // AASDK_DEBUG_NETWORK_DETAILED // AASDK_DEBUG_USER_INTERACTION // AASDK_DEBUG_GENERAL, // AASDK_DEBUG_USER_INTERACTION AASDK.registerDebugListeners(for: self, forMessageTypes: debug) return true } } ``` #### ** Objective-C ** ```objc // In your .m file @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // ... [AASDK startSessionWithAppID:@"API_KEY" registerListenersFor:self options:options]; // Setup for optional debug listener must be AFTER the startSession call above. NSArray *debug = @[ AASDK.DEBUG_ALL ]; // AASDK_DEBUG_NETWORK_DETAILED // AASDK_DEBUG_USER_INTERACTION // AASDK_DEBUG_GENERAL, // AASDK_DEBUG_USER_INTERACTION [AASDK registerDebugListenersFor:self forMessageTypes:(debug)]; return YES; } @end ``` You will also extend the AppDelegate to handle the `AASDKDebugObserver` and define a callback function as follows: #### ** Swift ** ```swift class AppDelegate: AASDKDebugObserver { func aaDebugNotification(_ notification: Notification) { print("debug: " + notification.debugDescription) } } ``` #### ** Objective-C ** ```objc // In your .h file @interface AppDelegate : UIResponder @property (strong, nonatomic) UIWindow *window; @end // In your .m file - (void)aaDebugNotification:(nonnull NSNotification *)notification { NSLog(@"%@", notification); } ``` **Implementation with ObjC++** Importing frameworks via the `@import` syntax isn't supported out of the box in Objective-C++, so you'll have to add the **-fcxx-modules** compiler switch to the Xcode build settings. Once completed, you may receive an error because the automatically-generated Obj-C header contains "non-modular" includes, specifically system headers like *stdbool.h* and so on. You'll have to turn on the `CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES` setting in the XCode project file to suppress this error. Otherwise it should import and run as normal. --- ### Testing ## Enabling Test Mode All testing can be done in the DEV environment. To enable testing, you can pass in the `AASDK.OPTION_TEST_MODE` option when starting the SDK session as follows: #### ** Swift ** ```swift let options = [ // When your app is ready for release, change or remove this flag AASDK.OPTION_TEST_MODE:true, ] as [String : Any] ``` #### ** Objective-C ** ```objc NSDictionary *options = @{ // When your app is ready for release, change or remove this flag AASDK.OPTION_TEST_MODE:@YES }; ``` > **Note:** Once your app is ready to release, you will need to disable the AASDK.OPTION_TEST_MODE option by either changing the value to false (Swift) or @NO (Objective-C) or by completely removing the option. ## In-App Ads To test In-App Ads, you will want to make sure your app is properly parsing the `AdContent` object by confirming the following: - Product name (as well as any other data) is available from the available `AADetailedListItem` objects. - For Add to List ads, make sure they correctly add the products to the list. > **Note:** Make sure to test both single and multiple product Add to List ads if both are available. ## Keyword Intercept > **Note:** When beginning development on Keyword Intercept, please confirm with AdAdapted that a keyword ad is active for your app. You will be provided with a list of valid keywords for testing purposes. To test your implementation of Keyword Intercept, you will need to start a product search in your app and provide at least 3 characters before a keyword product will be available for display. The following cases should be tested to ensure this feature is working as expected: - Three or more characters that satisfy an available keyword are provided while searching: - The keyword is found - The app properly displays the keyword option to the user for selection - Clicking the keyword result adds the item to the users list - Three or more characters that do not satisfy an available keyword are provided while searching: - The keyword is not found - The app does not display a keyword option to the user for selection - Less than three characters are provided while searching: - No keyword result is provided - The app will not display a keyword option to the user for selection ## Out-of-App Ads > **Note:** When beginning development on out-of-app Add-It, please confirm with AdAdapted that a payload and test link has been prepared for your app. Using the out-of-app test link provided by AdAdapted, you will be able to schedule a payload or trigger a deeplink. Once you are ready to begin the testing phase, this app-specific link (example shown above) can be used to schedule a payload or fire a universal link. The following is a screen capture of the view you will see when using the testing link and descriptions of what each button does: ![Out-Of-App Ad Payload Test](../images/out-of-app-ad-payload-test.png) | Button | Description | | ------ | ----------- | | `Schedule` | From a web browser on a computer or the test device, navigate to the test link URL and enter the device's advertising ID (IDFA), then click the `Schedule` button. The next time your app is opened the payload will be processed as defined in the app. | | `Fire Deeplink` | From a web browser on the test device, navigate to the test link URL and click the `Fire Deeplink` button. Your app should open immediately and the payload will be processed as defined in the app. | ## AADetailedListItem Data Model Ad content comes from the SDK in the form of a JSON payload. Both in-app and out-of-app content is returned as a list of one or more `AADetailedListItem` objects. Each `AADetailedListItem` contains several pieces of information about the product(s) in the current ad. While the `productTitle` field will always be present, other fields may or may not contain data. The following defines the structure of an `AADetailedListItem` object: ``` { productTitle: string; productBrand: string; productCategory: string; productDescription: string; productDiscount: string; productImageURL: string; productUpc: string; retailerSku: string; } ``` --- ### Upgrade Guide ## Changes from Objective-C SDK If you're coming from the Objective-C SDK, there are a few major differences to be aware of when switching over. The Swift SDK is only available now via CocoaPods installation under the pod name: 'AASwiftSDK'. How to set this up is explained further in the Getting Started portion of these documents. The SDK also utilizes features from Swift 5, which may require you to update your '*Swift Language Version*' under *Build Settings > Swift Compiler - Language*. Below highlights a few more code specific changes that will need to be updated: - Zone hookup is now just called AdAdaptedZoneView instead of AAAdAdaptedZoneView - Setting ZoneOwner is now a method rather than a property: setZoneOwner(self) #### ** Swift ** ```swift adAdaptedZoneView.setZoneOwner(self) ``` #### ** Objective-C ** ```objc [_adZone setZoneOwner:self]; ``` - All of the Keys are no longer prefixed with AASDK, but instead are called from the AASDK class. Example: AASDK_KEY_AD_CONTENT is now AASDK.KEY_AD_CONTENT #### ** Swift ** ```swift let adContent = userinfo[AASDK.KEY_AD_CONTENT] ``` #### ** Objective-C ** ```objc AdContent *adContent = [[notification userInfo] objectForKey:AASDK.KEY_AD_CONTENT]; ``` - Universal Link hookup is now in the SceneDelegate file rather than AppDelegate: #### ** Swift ** ```swift func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { AASDK.linkContentParser(userActivity) } ``` #### ** Objective-C ** ```objc - (void)scene:(UIScene *)scene continueUserActivity:(nonnull NSUserActivity *)userActivity { [AASDK linkContentParser:userActivity]; } ``` Any other setup needs should be handled in the [Getting Started](docs/swift-getting-started.md) or [Usage](docs/swift-usage.md) guides within this documentation. --- ## Payload as a Service SDK ### Getting Started # Getting Started This SDK exposes methods that allow you to quickly interface with the Payload as a Service API without having to write your own `XMLHttpRequest` for each API endpoint you need to use. ## Features ### Known User - User Payload Retrieval - Remove User Payloads - Describe Payload - Schedule Payload - Schedule Dynamic Payload - Record Impression - Record User Order ### Unknown User - Add to Cart - Add to List - Clip Offer - Retailer Method Integration - Customizable Action Modal ### Campaign API - Create Campaign - Get Campaign Details ## Installation You can install this SDK use one of the two following methods: #### ** Module ** ```bash npm install --save @adadapted/payload-service-sdk ``` #### ** Script ** ```js ``` ## License [AdAdapted Platform License](docs/license.md) --- ### Usage # Usage Follow the same approach you chose for installation of this SDK from [Getting Started](docs/payload-as-a-service-getting-started?id=installation) (`Module` or `Script`). --- ## Module Approach > **Note:** This approach includes a .d.ts file for Typescript support. Start by importing the `AdAdapatedPayloadServiceSdk` class. You should do this in your main component file, so you are defining the SDK reference once and passing down the reference as needed through props. ```js import AdAdapatedPayloadServiceSdk from "@adadapted/payload-service-sdk/src/index"; ``` Next, initialize the Payload as a Service SDK. Do this on load of your component. > **Note:** Make sure to replace the placeholder YOUR_SDK_KEY with your actual SDK key provided by AdAdapted. ```js const sdk = new AdAdapatedPayloadServiceSdk({ sdkKey: "YOUR_SDK_KEY", sdkEnv: "prod", // Optional: retailer-specific settings retailerSettings: { allowOutOfStockAddToCart: false, userConfirmAddToCart: false, }, // Optional: known user feature callbacks knownUserFeatures: { callbacks: { onRetrieveUserPayloads: { success: (response) => { // TODO: handle successful response. }, error: () => { // TODO: handle error response. }, }, onRemoveUserPayloads: { success: () => { // TODO: handle successful response. }, error: () => { // TODO: handle error response. }, }, onRecordUserOrder: { success: () => { // TODO: handle successful response. }, error: () => { // TODO: handle error response. }, }, }, }, // Optional: unknown user features // See Unknown User Features docs for full configuration. unknownUserFeatures: { // ... }, }); ``` You can then store `sdk` to your components state or leave it as a global variable as needed. > **Note:** For full details on unknown user configuration — including actionRequiresLogin, actionComplete callbacks, and methods — see Unknown User Features. For retailer method implementation details, see Retailer Methods. --- ## Script Approach First, add a script that defines the `onPayloadSdkAvailable()` method. This method will be called once the SDK has finished downloading from the AdAdapted CDN. > **Note:** Make sure to replace the placeholder YOUR_SDK_KEY with your actual SDK key provided by AdAdapted. ```js ``` Next, add the script tag that imports the SDK with an `onload` handler pointing to the method defined above. This must appear **after** the inline script so that `onPayloadSdkAvailable` is defined before the SDK finishes loading. ```js ``` --- ### Known User Methods # Known User Methods ## constructor() The constructor accepts a single initialization object: ```js constructor( initDetails: AdAdapatedPayloadServiceSdk.InitializationDetails ); ``` - sdkKey (required): The key used to access Payload as a Service. This is provided to you by AdAdapted. - sdkEnv (optional): The environment to connect to. Defaults to `"prod"`. - retailerSettings (optional): Retailer-specific behavior configuration. - knownUserFeatures (optional): Callbacks for known user API requests. - unknownUserFeatures (optional): Configuration for unknown user actions. See [Unknown User Features](docs/payload-as-a-service-unknown-user-features.md) for details. ```js { sdkKey: string; sdkEnv?: "dev" | "stg" | "prod"; retailerSettings?: RetailerSettings; knownUserFeatures?: KnownUserFeatures; unknownUserFeatures?: UnknownUserFeatures; } ``` The following code block defines the structure of the `KnownUserFeatureCallbacks` type, provided within `knownUserFeatures.callbacks`. Each top-level and child property is optional, so you can choose the callbacks you want to use. ```js { onDescribePayload?: { success?: ((payload: Payload) => void); error?: (() => void); }; onRetrieveUserPayloads?: { success?: ((payload: Payload[]) => void); error?: (() => void); }; onRemoveUserPayload?: { success?: (() => void); error?: (() => void); }; onRemoveUserPayloads?: { success?: (() => void); error?: (() => void); }; onRecordUserOrder?: { success?: (() => void); error?: (() => void); }; onRecordImpression?: { success?: (() => void); error?: (() => void); }; onSchedulePayload?: { success?: (() => void); error?: (() => void); }; onScheduleDynamicPayload?: { success?: (() => void); error?: (() => void); }; } ``` The next code block defines the structure of the `Payload` type used above as the resulting data for the `onDescribePayload` and `onRetrieveUserPayloads` success callbacks. ```js { account: string; campaignId: string; userId: string; payloadUuid: string; payloadVersionId: string; customData: { [key: string]: | string | number | boolean | [string | number | boolean] | { [key: string]: string | number | boolean | [string | number | boolean]; }; }; } ``` --- ## getSdkConfiguration() This method will return your current configuration of the SDK. ```js getSdkConfiguration(): AdAdapatedPayloadServiceSdk.SdkConfiguration; ``` The next code block defines the structure of the `SdkConfiguration` type used above as the return type of the `getSdkConfiguration()` method. ```js { apiKey: string; apiEnv: string; envUrl: string; callbacks: AdAdapatedPayloadServiceSdk.Callbacks; } ``` --- ## describePayload() This method will return a description of a payload based on the provided payload ID. This is intended for use during implementation of this SDK in your application. It allows you to verify that the payload exists and see the structure to expect once you begin using the [retrieveUserPayloads()](docs/payload-as-a-service-methods?id=retrieveuserpayloads) method. ```js describePayload( payloadId: string ): void; ``` You will only get the response if you implemented the callback for the `Callbacks.onDescribePayload.success` and/or `Callbacks.onDescribePayload.error` as described in the [constructor()](docs/payload-as-a-service-methods?id=constructor) section defining all callback structures. > **Note:** The userId property in the response data will contain the macro used for userID and not an actual user ID. --- ## retrieveUserPayloads() This method will get all currently available payloads for a user based on the provided `data`. The `data` parameter can either be a single user ID or an array of all known IDs for a single user. AdAdapted will hash the provided user ID values using the agreed upon hashing algorithm when you set the `makeHash` parameter to true. > **Note:** Only set the makeHash parameter to false if you are already pre-hashing user ID values being provided to this method. ```js retrieveUserPayloads( inputs: AdAdapatedPayloadServiceSdk.RetrieveUserPayloadsInputs ): void; ``` The next code block defines the structure of the `RetrieveUserPayloadsInputs` type used above as the input type of the method. ```js { data: string | string[]; makeHash: boolean; } ``` You will only get the response if you implemented the callback for the `Callbacks.onRetrieveUserPayloads.success` and/or `Callbacks.onRetrieveUserPayloads.error` as described in the [constructor()](docs/payload-as-a-service-methods?id=constructor) section defining all callback structures. --- ## removeUserPayload() This method will remove a specific payload for a user based on the provided `userId` and `payloadId`. AdAdapted will hash the provided user ID value using the agreed upon hashing algorithm when you set the `makeHash` parameter to true. > **Note:** Only set the makeHash parameter to false if you are already pre-hashing user ID values being provided to this method. ```js removeUserPayload( inputs: AdAdapatedPayloadServiceSdk.RemoveUserPayloadInputs ): void; ``` The next code block defines the structure of the `RemoveUserPayloadInputs` type used above as the input type of the method. ```js { userId: string; payloadId: string; payloadHashId: string | null | undefined; makeHash: boolean; } ``` You will only get the response if you implemented the callback for the `Callbacks.onRemoveUserPayload.success` and/or `Callbacks.onRemoveUserPayload.error` as described in the [constructor()](docs/payload-as-a-service-methods?id=constructor) section defining all callback structures. --- ## removeUserPayloads() This method will remove all payloads for a user based on the provided `userId`. AdAdapted will hash the provided user ID value using the agreed upon hashing algorithm when you set the `makeHash` parameter to true. > **Note:** Only set the makeHash parameter to false if you are already pre-hashing user ID values being provided to this method. ```js removeUserPayloads( inputs: AdAdapatedPayloadServiceSdk.RemoveUserPayloadsInputs ): void; ``` The next code block defines the structure of the `RemoveUserPayloadsInputs` type used above as the input type of the method. ```js { userId: string; makeHash: boolean; } ``` You will only get the response if you implemented the callback for the `Callbacks.onRemoveUserPayloads.success` and/or `Callbacks.onRemoveUserPayloads.error` as described in the [constructor()](docs/payload-as-a-service-methods?id=constructor) section defining all callback structures. --- ## recordUserOrder() This method will record a user's placed order based on the provided `userId` and the provided user's `orderData`. AdAdapted will hash the provided user ID value using the agreed upon hashing algorithm when you set the `makeHash` parameter to true. > **Note:** Only set the makeHash parameter to false if you are already pre-hashing user ID values being provided to this method. ```js recordUserOrder( inputs: AdAdapatedPayloadServiceSdk.RecordUserOrderInputs ): void; ``` The next code block defines the structure of the `RecordUserOrderInputs` type used above as the input type of the method. ```js { userId: string; orderData: AdAdapatedPayloadServiceSdk.UserOrderData; makeHash: boolean; } ``` The next code block defines the structure of the `UserOrderData` type used above as an input to the `recordUserOrder()` method. ```js { orderId: string; orderTs: string; items: AdAdapatedPayloadServiceSdk.UserOrderItem[]; } ``` The next code block defines the structure of the `UserOrderItem` type used above in the `UserOrderData` type. ```js { upc: string; name: string; category: string; subcategory: string; quantity: number; amount: number; payloadId: string; } ``` You will only get the response if you implemented the callback for the `Callbacks.onRecordUserOrder.success` and/or `Callbacks.onRecordUserOrder.error` as described in the [constructor()](docs/payload-as-a-service-methods?id=constructor) section defining all callback structures. --- ## recordImpression() > **Note:** USE NOT RECOMMENDED Only use this method if you are not using the auto-generated tag provided by the https://payloads.adadapted.com site. The auto-generated tag already performs this action for you. This method is only intended for the purpose of self-managing your ad unit interactions with Payload as a Service and controlling when the impression occurs. This method will record a user impression for a payload based on the provided `userId` and `payloadId`. AdAdapted will hash the provided user ID value using the agreed upon hashing algorithm when you set the `makeHash` parameter to true. > **Note:** Only set the makeHash parameter to false if you are already pre-hashing user ID values being provided to this method. ```js recordImpression( inputs: AdAdapatedPayloadServiceSdk.RecordImpressionInputs ): void; ``` The next code block defines the structure of the `RecordImpressionInputs` type used above as the input type of the method. ```js { userId: string; payloadId: string; auctionId: string; makeHash: boolean; } ``` You will only get the response if you implemented the callback for the `Callbacks.onRecordImpression.success` and/or `Callbacks.onRecordImpression.error` as described in the [constructor()](docs/payload-as-a-service-methods?id=constructor) section defining all callback structures. --- ## schedulePayload() > **Note:** USE NOT RECOMMENDED Only use this method if you are not using the auto-generated tag provided by the https://payloads.adadapted.com site. The auto-generated tag already performs this action for you. This method is only intended for the purpose of self-managing your ad unit interactions with Payload as a Service and controlling when the scheduling of a payload occurs. This method will schedule a payload for a user based on the provided `userId` and `payloadId`. AdAdapted will hash the provided user ID value using the agreed upon hashing algorithm when you set the `makeHash` parameter to true. > **Note:** Only set the makeHash parameter to false if you are already pre-hashing user ID values being provided to this method. ```js schedulePayload( inputs: AdAdapatedPayloadServiceSdk.SchedulePayloadInputs ): void; ``` The next code block defines the structure of the `SchedulePayloadInputs` type used above as the input type of the method. ```js { userId: string; payloadId: string; auctionId: string; makeHash: boolean; } ``` You will only get the response if you implemented the callback for the `Callbacks.onSchedulePayload.success` and/or `Callbacks.onSchedulePayload.error` as described in the [constructor()](docs/payload-as-a-service-methods?id=constructor) section defining all callback structures. --- ## scheduleDynamicPayload() > **Note:** USE NOT RECOMMENDED Only use this method if you are not using the auto-generated tag provided by the https://payloads.adadapted.com site. The auto-generated tag already performs this action for you. This method is only intended for the purpose of self-managing your ad unit interactions with Payload as a Service and controlling when the scheduling of a payload occurs. This method will schedule a payload for a user based on the provided `userId`, `payloadId`, and `macroToValueMappingList`. AdAdapted will hash the provided user ID value using the agreed upon hashing algorithm when you set the `makeHash` parameter to true. > **Note:** Only set the makeHash parameter to false if you are already pre-hashing user ID values being provided to this method. ```js scheduleDynamicPayload( inputs: AdAdapatedPayloadServiceSdk.ScheduleDynamicPayloadInputs ): void; ``` The next code block defines the structure of the `ScheduleDynamicPayloadInputs` type used above as the input type of the method. ```js { userId: string; payloadId: string; auctionId: string; makeHash: boolean; macroToValueMappingList: { [key: string]: string | number | boolean }[]; } ``` > **Note:** The macroToValueMappingList is a key/value pairing of macro(the key) and the value to use in place of the macro within a payload structure. You will only get the response if you implemented the callback for the `Callbacks.onScheduleDynamicPayload.success` and/or `Callbacks.onScheduleDynamicPayload.error` as described in the [constructor()](docs/payload-as-a-service-methods?id=constructor) section defining all callback structures. --- ### Unknown User Features # Unknown User Features The Payload as a Service SDK supports actions for users who have not yet been identified, such as shoppers arriving from an ad click. These are called **unknown user features** and enable actions like Add to Cart, Add to List, and Clip Offer to be performed directly on a retailer's website without requiring the user to be pre-registered in the system. The end-to-end flow works as follows: 1. A campaign is created via the [Campaign API](docs/payload-as-a-service-campaign-api.md), which returns a redirect URL. 2. The redirect URL is used as the ad clickthrough in the ad platform. 3. A user clicks the ad. The [redirect service](docs/payload-as-a-service-campaign-api?id=redirect-service) transforms the campaign token into an SDK-compatible `aa_flux` token and redirects the user to the retailer's website. 4. The SDK reads the `aa_flux` parameter on initialization and fetches the full payload from the AdAdapted API. 5. The SDK calls `getProductDetails` to resolve product metadata and availability. 6. The SDK renders an interactive modal presenting the actions to the user. 7. When the user confirms, the SDK calls the appropriate retailer method (`addToCart`, `addToList`, or `clipOffer`). 8. The `actionComplete` callback fires with the result. > **Note:** The aa_flux parameter is read once during SDK initialization. If your site is a single-page application, ensure the parameter is preserved in the URL until the SDK has been initialized. After initialization, the SDK stores payload data in localStorage for subsequent processing. --- ## Configuration Unknown user features are configured through the `unknownUserFeatures` property of the SDK constructor. For the full constructor reference, see [Usage](docs/payload-as-a-service-usage.md). ```js const sdk = new AdAdapatedPayloadServiceSdk({ sdkKey: "YOUR_SDK_KEY", sdkEnv: "prod", unknownUserFeatures: { actionRequiresLogin: { addToCart: false, addToList: true, clipOffer: true, }, callbacks: { actionComplete: { addToCart: (success) => { console.log("Add to cart completed:", success); }, addToList: (success) => { console.log("Add to list completed:", success); }, clipOffer: (success) => { console.log("Clip offer completed:", success); }, }, loginRequested: () => { window.location.href = "/login?returnUrl=" + encodeURIComponent(location.href); }, }, methods: { isUserAuthenticated: () => { /* ... */ }, getProductDetails: (skuList) => { /* ... */ }, addToCart: (productList) => { /* ... */ }, addToList: (productList) => { /* ... */ }, clipOffer: (offerIdList) => { /* ... */ }, }, modalSettings: { // optional UI customization }, }, }); ``` --- ## actionRequiresLogin Controls whether each action type requires the user to be authenticated before the SDK will execute it. If an action requires login and the user is not authenticated, the SDK will invoke the `loginRequested` callback instead. ```js { addToCart?: boolean; addToList?: boolean; clipOffer?: boolean; } ``` | Property | Type | Default | Description | |----------|------|---------|-------------| | `addToCart` | `boolean` | `false` | If `true`, Add to Cart requires the user to be logged in. | | `addToList` | `boolean` | `false` | If `true`, Add to List requires the user to be logged in. | | `clipOffer` | `boolean` | `false` | If `true`, Clip Offer requires the user to be logged in. | > **Note:** Set an action to false if the retailer supports guest execution for that action (e.g., a guest cart). --- ## Callbacks ### actionComplete Optional callbacks invoked when an unknown user action finishes. Each receives a `boolean` indicating whether the action was executed. - `true` — the retailer method was called (e.g., the user was authenticated or login was not required). - `false` — the action could not be performed (e.g., login was required but the user is not authenticated, user confirmation is pending, or no items were available to process). ```js { actionComplete?: { addToCart?: ((success: boolean) => void) | null; addToList?: ((success: boolean) => void) | null; clipOffer?: ((success: boolean) => void) | null; }; } ``` ### loginRequested Invoked when the SDK determines the user must log in before an action can proceed. Use this to redirect to the retailer's login flow. ```js { loginRequested?: (() => void) | null; } ``` --- ## Methods The `methods` property provides the SDK with retailer-specific implementations for authentication, product lookup, and action execution. These are required for unknown user features to function. ```js { isUserAuthenticated?: (() => Promise) | null; getProductDetails?: ((sku: string[]) => Promise) | null; addToCart?: ((productList: AddToCartItem[]) => Promise) | null; addToList?: ((productList: AddToListItem[]) => Promise) | null; clipOffer?: ((offerIdList: string[]) => Promise) | null; } ``` For full method signatures, parameter types, return types, and implementation examples, see [Retailer Methods](docs/payload-as-a-service-retailer-methods.md). --- ## Retailer Settings The `retailerSettings` property controls behavior related to unknown user actions. It is a **top-level** property on the SDK constructor — not nested inside `unknownUserFeatures`. See [Usage](docs/payload-as-a-service-usage.md) for its placement in the constructor. ```js { retailerSettings?: { allowOutOfStockAddToCart?: boolean; userConfirmAddToCart?: boolean; }; } ``` | Property | Type | Default | Description | |----------|------|---------|-------------| | `allowOutOfStockAddToCart` | `boolean` | `false` | If `true`, items may be added to cart even when reported as out of stock. | | `userConfirmAddToCart` | `boolean` | `false` | If `true`, the user must confirm Add to Cart in the modal before the action executes. | --- ## Modal Settings The SDK displays an interactive modal for unknown user actions. You can customize the modal's appearance through the `modalSettings` property within the `unknownUserFeatures` configuration. ```js { modalSettings?: { mainHeaderTextColor?: string; salePriceTextColor?: string; successColor?: string; attentionColor?: string; warningColor?: string; outOfStockBadgeColor?: string; productUnavailableMessageColor?: string; alternateProductMessageColor?: string; primaryButtonBackgroundColor?: string; primaryButtonTextColor?: string; primaryButtonBorderColor?: string; secondaryButtonBackgroundColor?: string; secondaryButtonTextColor?: string; secondaryButtonBorderColor?: string; buttonBorderRadius?: string; buttonBorderWidth?: string; modalContainerBorderRadius?: string; disclaimerText?: string; productUnavailableMessage?: string; alternateProductMessage?: string; }; } ``` All properties are optional. If not provided, the SDK uses its default styling. ### Color Properties | Property | Description | |----------|-------------| | `mainHeaderTextColor` | Color for the modal's main header text. | | `salePriceTextColor` | Color for items displaying a sale price. | | `successColor` | Color for icons and text in a "success" state. | | `attentionColor` | Color for icons and text in an "attention" state. | | `warningColor` | Color for icons and text in a "warning" state. | | `outOfStockBadgeColor` | Color for the out-of-stock badge on items. | | `productUnavailableMessageColor` | Color for the product unavailable message. | | `alternateProductMessageColor` | Color for the alternate product message. | ### Button Properties | Property | Description | |----------|-------------| | `primaryButtonBackgroundColor` | Background color for primary buttons. | | `primaryButtonTextColor` | Text color for primary buttons. | | `primaryButtonBorderColor` | Border color for primary buttons. | | `secondaryButtonBackgroundColor` | Background color for secondary buttons. | | `secondaryButtonTextColor` | Text color for secondary buttons. | | `secondaryButtonBorderColor` | Border color for secondary buttons. | | `buttonBorderRadius` | CSS border-radius for buttons (e.g., `"8px"`). | | `buttonBorderWidth` | CSS border-width for buttons. | ### Container Properties | Property | Description | |----------|-------------| | `modalContainerBorderRadius` | CSS border-radius for the modal container (e.g., `"8px"`). | ### Text Properties | Property | Description | |----------|-------------| | `disclaimerText` | Disclaimer text displayed below successful actions. If not provided, no disclaimer is shown. | | `productUnavailableMessage` | Message displayed when a product is unavailable. If not provided, a default message is used. | | `alternateProductMessage` | Message displayed when an alternate product is shown. If not provided, a default message is used. | --- ## updateModalSettings() You can update the modal settings at any time after initialization using the `updateModalSettings()` method on the SDK instance. ```js sdk.updateModalSettings({ primaryButtonBackgroundColor: "#0a4d8c", primaryButtonTextColor: "#ffffff", disclaimerText: "Please ensure all items and quantities are correct.", }); ``` --- ### Retailer Methods # Retailer Methods These are the methods you provide in the `unknownUserFeatures.methods` configuration. The SDK calls them to delegate authentication checks, product lookups, and shopper actions to your platform. All methods are optional but required for their corresponding action to function. For example, if you want to support Add to Cart, you must provide both `getProductDetails` and `addToCart`. > **Note:** The SDK does not wrap retailer method calls in try/catch. If a method rejects its promise, the rejection will propagate as an unhandled promise rejection. Always resolve your promises — use internal try/catch to handle errors and resolve gracefully rather than rejecting. --- ## isUserAuthenticated() Determines whether the current shopper is authenticated. The SDK uses this to decide whether an action can run immediately or requires a login redirect. ```js isUserAuthenticated(): Promise ``` **Parameters:** None. **Returns:** `Promise` — resolves `true` if the shopper is authenticated, `false` otherwise. > **Note:** Resolve false for unauthenticated, expired sessions, or any error condition. Never reject the promise — always resolve. **Implementation approaches:** - **Cookie-based:** Check for the presence of a session or auth cookie. This is the simplest approach when the retailer sets a known cookie upon login. - **Endpoint-based:** Call a lightweight, deterministic endpoint (e.g., `/api/whoami` or `/api/account/summary`) with `credentials: "include"`. Treat HTTP 200 with a valid user payload as `true`, and HTTP 401/403 or anonymous/guest responses as `false`. **Example (cookie-based):** ```js isUserAuthenticated: () => { return new Promise((resolve) => { const match = document.cookie.match(/(^| )authToken=([^;]+)/); resolve(!!match); }); } ``` **Example (endpoint-based):** ```js isUserAuthenticated: async () => { try { const response = await fetch("/api/whoami", { credentials: "include" }); if (!response.ok) return false; const data = await response.json(); return !!data.userId; } catch { return false; } } ``` --- ## getProductDetails() Provides the SDK with product metadata used to render the modal (name, image, pricing) and evaluate stock availability for fallback logic. ```js getProductDetails(skuList: string[]): Promise ``` **Parameters:** | Name | Type | Description | |------|------|-------------| | `skuList` | `string[]` | Product identifiers (UPC, SKU, or internal ID) to look up. These originate from the campaign's payload data embedded in the redirect URL. | **Returns:** `Promise` — an array with one entry per requested identifier. ### ProductDetail ```js { sku: string; // required — echoes the input identifier stockCount: number; // required — 0 = out of stock, >0 = available name?: string; // display name for the modal imageUrl?: string; // product image URL regularPrice?: number; // numeric regular price salePrice?: number; // numeric sale price (omit if not applicable) } ``` > **Note:** For products that cannot be found, return a minimal entry with stockCount: 0 or omit the entry. Never reject the promise — always resolve with an array (possibly empty). **Implementation notes:** - Use `credentials: "include"` if pricing or availability is personalized to the user's session. - **Price parsing:** If the retailer API returns prices as strings (e.g., `"$3.99"`, `"USD 3.99"`), strip non-numeric characters and parse to a number. - **Stock count:** Use the explicit stock count when available. If the API only provides a boolean or status field (e.g., `"IN_STOCK"` / `"OUT_OF_STOCK"`), derive a numeric value: `0` for out of stock, or a positive sentinel (e.g., `1` or `1000`) when in stock but the exact quantity is not reliable. **Example:** ```js getProductDetails: async (skuList) => { const results = []; for (const sku of skuList) { try { const response = await fetch(`/api/products/${sku}`, { credentials: "include", }); const product = await response.json(); results.push({ sku: sku, name: product.name || product.title, imageUrl: product.images?.[0]?.url, regularPrice: parseFloat(product.price) || undefined, salePrice: parseFloat(product.salePrice) || undefined, stockCount: product.stockCount ?? (product.inStock ? 1 : 0), }); } catch { results.push({ sku, stockCount: 0 }); } } return results; } ``` --- ## addToCart() Adds one or more products to the shopper's cart. ```js addToCart(productList: AddToCartItem[]): Promise ``` **Parameters:** | Name | Type | Description | |------|------|-------------| | `productList` | `AddToCartItem[]` | Products to add to the cart. | ### AddToCartItem ```js { sku: string; // product identifier quantity: number; // number of items to add } ``` **Returns:** `Promise` — resolve when the operation completes. The SDK treats resolution as success. > **Note:** Resolve without throwing even if some items fail (partial success is permitted). The SDK will handle reporting to the user. **Implementation notes:** - Use `credentials: "include"` (required for session-based carts). - If the retailer requires CSRF tokens, include them in request headers or form fields. - **Input normalization:** Trim whitespace from SKU values and default quantity to `1` when missing or invalid. - **Payload patterns:** Batch endpoints (sending all items in a single request) are preferred for performance, but per-item requests are acceptable. **Example (batch endpoint):** ```js addToCart: async (productList) => { await fetch("/api/cart/items", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items: productList.map((item) => ({ sku: String(item.sku).trim(), quantity: item.quantity || 1, })), }), }); } ``` **Example (per-item):** ```js addToCart: async (productList) => { for (const item of productList) { await fetch("/api/cart/add?sku=" + encodeURIComponent(item.sku) + "&quantity=" + item.quantity, { method: "POST", credentials: "include", }); } } ``` --- ## addToList() Adds items to the shopper's shopping list or saved list. Supports free-text adds (no catalog identifier required). ```js addToList(productList: AddToListItem[]): Promise ``` **Parameters:** | Name | Type | Description | |------|------|-------------| | `productList` | `AddToListItem[]` | Items to add to the list. | ### AddToListItem ```js { name?: string; // item name (required if sku is not provided) sku?: string; // item SKU (required if name is not provided) quantity?: number; // number of items (defaults to 1) } ``` > **Note:** Unlike Add to Cart, the SDK does not call getProductDetails for list items. Items may arrive with only a sku and no name. If your list UI displays item names, your implementation should resolve SKUs to display names. **Returns:** `Promise` **Implementation notes:** Two common patterns exist for list integration: - **(A) API-driven** (preferred): Send items as JSON via `POST` with `credentials: "include"`. - **(B) Form-post:** For legacy list implementations, use `application/x-www-form-urlencoded` with list context fields derived from the DOM. **Example (API-driven):** ```js addToList: async (productList) => { await fetch("/api/list/items", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items: productList.map((item) => ({ name: item.name || item.sku, quantity: item.quantity || 1, })), }), }); } ``` --- ## clipOffer() Clips or activates digital offers on the shopper's account. ```js clipOffer(offerIdList: string[]): Promise ``` **Parameters:** | Name | Type | Description | |------|------|-------------| | `offerIdList` | `string[]` | Offer identifiers to clip for the user. | **Returns:** `Promise` **Implementation notes:** - Use `credentials: "include"` (required for account-level offers). - If the retailer requires CSRF tokens, include them in request headers. - Batch endpoints (sending all offer IDs in a single request) are preferred when available. - Treat "already clipped" as a success condition where possible. **Example (per-offer):** ```js clipOffer: async (offerIdList) => { for (const offerId of offerIdList) { await fetch(`/api/offers/${offerId}/clip`, { method: "POST", credentials: "include", }); } } ``` **Example (batch):** ```js clipOffer: async (offerIdList) => { await fetch("/api/offers/clip", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ offerIds: offerIdList }), }); } ``` --- ### Campaign API # Campaign API The Campaign API enables server-side creation and retrieval of advertising campaigns within the AdAdapted ecosystem. It ingests, validates, and stores campaign metadata, transforming it into actionable shopper interactions such as Add to Cart, Add to List, and Clip Offer. Upon successful campaign creation, the API returns a unique redirect URL that enables clickthrough tracking and downstream shopper conversion. > **Note:** This is a server-side REST API, separate from the client-side Payload as a Service SDK. Use this API to manage campaigns programmatically. --- ## Authentication All requests must include a valid API key in the `x-api-key` header. ``` x-api-key: API_KEY ``` > **Note:** Your API key is provided by AdAdapted during onboarding. If your integration requires scoped access, ensure the key has the appropriate permissions for the target retailer and ad channel. --- ## Environments | Environment | Base URL | |-------------|----------| | Production | `https://api.payloads.adadapted.com` | | Development | `https://api.dev.payloads.adadapted.dev` | --- ## Response Envelope All responses follow a consistent structure with three top-level keys: ```js { "data": { ... }, // Response payload on success, null on error "warnings": [], // Array of non-blocking warnings (success responses only) "error": null // Error object on failure, null on success } ``` --- ## Create Campaign Creates a new campaign and returns a redirect URL for use as the ad clickthrough. **Method:** `POST` **Endpoint:** `/management/campaigns` **Content-Type:** `application/json` ### Request Body The request accepts both `snake_case` and `camelCase` field names. ```js { "ad_channel": "meta", "partner_id": "550e8400-e29b-41d4-a716-446655440000", "retailer_id": "retailer-com", "campaign_id": "summer-grilling-2026", "campaign_name": "Summer Grilling June 1st", "advertiser_name": "Acme Cereal Co", "landing_page_url": "https://retailer.com/product/123456", "campaign_expiry": "2026-12-31T23:59:59Z", "tracking_tags": { "on_click": ["https://tracker.example.com/click"], "action_complete": ["https://tracker.example.com/conversion"] }, "actions": [ { "type": "add_to_cart", "items": [ { "sku": "123", "quantity": 1, "alternatives": [ { "sku": "124", "quantity": 1 }, { "sku": "125", "quantity": 2 } ] }, { "sku": "456", "quantity": 1 } ] }, { "type": "add_to_list", "list_items": [ { "name": "Ripe bananas", "quantity": 2 }, { "sku": "123456789012" }, { "name": "Flour" } ] }, { "type": "clip_offer", "offer_ids": ["offer-789"] } ] } ``` ### Core Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `ad_channel` | `string` | Yes | Ad channel identifier (e.g., `"meta"`, `"pinterest"`, `"dv360"`). | | `partner_id` | `string` (UUID) | Yes | Your partner identifier, provided by AdAdapted. | | `retailer_id` | `string` | Yes | Retailer identifier (account slug). | | `campaign_id` | `string` | Yes | Your unique identifier for this campaign. Must be unique per partner. | | `campaign_name` | `string` | No | Human-readable campaign label. Auto-generated as `"Campaign-{campaign_id}"` if not provided. | | `advertiser_name` | `string` | No | Advertiser's name. | | `landing_page_url` | `string` (URL) | Yes | Final landing page URL on the retailer's site. | | `campaign_expiry` | `string` (ISO 8601) | Yes | Campaign end timestamp. | | `tracking_tags` | `object` | No | Tracking URLs for click and conversion events. | | `actions` | `array` | Yes | Array of action objects. At least one action must be provided. | ### Tracking Tags | Field | Type | Description | |-------|------|-------------| | `on_click` | `string[]` | URLs triggered when the ad is clicked. | | `action_complete` | `string[]` | URLs triggered when a shopper action completes. | ### Action Types Each action object must include a `type` field. The following types are supported: #### click A simple click-through action with no additional data. ```js { "type": "click" } ``` #### add_to_cart Adds item(s) to the shopper's cart. ```js { "type": "add_to_cart", "items": [ { "sku": "123", "quantity": 1, "alternatives": [ { "sku": "124", "quantity": 1 } ] } ] } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `items` | `array` | Yes | Array of SKU items. | | `items[].sku` | `string` | Yes | Retailer product SKU. | | `items[].quantity` | `integer` | Yes | Number of items to add. Minimum: 1. | | `items[].alternatives` | `array` | No | Ordered list of fallback products (same structure as items). | > **Note:** The order of items in the alternatives array determines the fallback priority used in case of out-of-stock or not-carried SKUs. #### add_to_list Adds item(s) to the shopper's list. ```js { "type": "add_to_list", "list_items": [ { "name": "Ripe bananas", "quantity": 2 }, { "sku": "123456789012" } ] } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `list_items` | `array` | Yes | Array of list items. | | `list_items[].name` | `string` | Conditional | Item name. Required if `sku` is not provided. | | `list_items[].sku` | `string` | Conditional | Retailer SKU. Required if `name` is not provided. May not be supported across all retailers. | | `list_items[].quantity` | `integer` | No | Number of items. Minimum: 1. Defaults to 1. | #### clip_offer Clips or activates offer(s) for the shopper. ```js { "type": "clip_offer", "offer_ids": ["offer-789"] } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `offer_ids` | `string[]` | Yes | Array of offer identifiers to activate. | ### Success Response (200) ```js { "data": { "aa_campaign_id": "ad165983-ed3b-4d9b-9d53-3248ac15b85d", "redirect_url": "https://redirect.adadapted.com/r/retailer-com?aa_flux=eyJ..." }, "warnings": [], "error": null } ``` | Field | Type | Description | |-------|------|-------------| | `aa_campaign_id` | `string` (UUID) | AdAdapted's unique identifier for the campaign. | | `redirect_url` | `string` (URL) | The URL to use as the clickthrough in the ad platform. | --- ## Get Campaign Details Retrieves full details of an existing campaign. **Method:** `GET` **Endpoint:** `/management/campaigns/{aa_campaign_id}` ### Path Parameters | Name | Type | Description | |------|------|-------------| | `aa_campaign_id` | `string` (UUID) | The campaign identifier returned from the Create endpoint. | ### Success Response (200) ```js { "data": { "aa_campaign_id": "ad165983-ed3b-4d9b-9d53-3248ac15b85d", "ad_channel": "meta", "partner_id": "550e8400-e29b-41d4-a716-446655440000", "retailer_id": "retailer-com", "campaign_id": "summer-grilling-2026", "campaign_name": "Summer Grilling June 1st", "advertiser_name": "Acme Cereal Co", "landing_page_url": "https://retailer.com/product/123456", "campaign_expiry": "2026-12-31T23:59:59Z", "actions": { "add_to_cart": { "items": [ { "sku": "123", "quantity": 1, "alternatives": [ { "sku": "124", "quantity": 1 } ] } ] }, "add_to_list": { "list_items": [ { "name": "Ripe bananas", "quantity": 2 } ] }, "clip_offer": { "offer_ids": ["offer-789"] } }, "redirect_url": "https://redirect.adadapted.com/r/retailer-com?aa_flux=eyJ..." }, "warnings": [], "error": null } ``` > **Note:** The actions field in the GET response is structured as an object keyed by action type, unlike the request body which uses an array. --- ## Redirect Service The `redirect_url` returned by the Create Campaign endpoint points to the AdAdapted redirect service. This service sits between the ad platform and the retailer's website, performing tracking and token transformation. Understanding this flow is important when configuring ads or testing locally. ### How it works When a user clicks the ad: 1. The browser hits the `redirect_url`, which contains an `aa_flux` token in an intermediate format: ```json {"aa_campaign_id": "ad165983-...", "ad_channel": "meta"} ``` 2. The redirect service validates the campaign, performs click tracking, and transforms the token into the format the SDK expects: ```json {"payloadId": "ad165983-...", "clickId": "1770070683.5e15ea6435ec.retailer-com"} ``` 3. The service responds with a **302 redirect** to the retailer's `landing_page_url`, appending the transformed `aa_flux` token and a `_cb` cache-buster parameter. ### Redirect URL parameters | Parameter | Required | Description | |-----------|----------|-------------| | `aa_flux` | Yes | Base64-encoded campaign token (included automatically in the `redirect_url`). | | Click ID param | No | Ad-channel-specific click ID parameter (e.g., `click_id`). If provided by the ad platform, it is used as the `clickId` in the transformed token. If absent, the service auto-generates one. | | All other params | No | Any additional query parameters appended to the `redirect_url` are passed through to the retailer's landing page. | > **Note:** When configuring the ad in your ad platform, use the redirect_url as-is as the clickthrough URL. If the ad platform supports click ID macros, append them as a query parameter — the redirect service will pick it up automatically. All other tracking parameters added by the ad platform will be preserved through the redirect. ### Local testing To test locally, set your campaign's `landing_page_url` to your local dev server (e.g., `http://localhost:3000`). Then open the `redirect_url` in a browser — the redirect service will transform the token and land you on your local page with a valid `aa_flux` parameter. > **Note:** Do not copy the aa_flux token from the Campaign API response directly into your URL. That token is in an intermediate format that the SDK does not recognize. Always go through the redirect_url so the redirect service can transform it into the SDK-compatible format. --- ## Error Responses All error responses follow this structure: ```js { "data": null, "error": { "http_status": 400, "code": "ERR_MISSING_FIELD", "message": "Required field 'partner_id' is missing from the request body.", "details": [ { "location": "$.partner_id", "code": "ERR_FIELD_REQUIRED", "message": "'partner_id' is required and must be a non-empty string." } ] } } ``` ### HTTP Status Codes | Status | Code | Description | |--------|------|-------------| | `401` | `ERR_UNAUTHORIZED` | API key is missing or invalid. | | `403` | `ERR_FORBIDDEN` | API key does not have access to the specified retailer or ad channel. | | `404` | `ERR_NOT_FOUND` | Campaign with the specified `aa_campaign_id` was not found. | | `409` | `ERR_DUPLICATE_CAMPAIGN` | A campaign with this `campaign_id` already exists for your partner. | | `422` | `ERR_INVALID_PAYLOAD` | One or more fields are invalid. | | `429` | `ERR_RATE_LIMITED` | Rate limit exceeded. Retry after the indicated period. | | `500` | `ERR_INTERNAL_SERVER` | An unexpected server error occurred. | ### Detail Error Codes The `details` array may include the following codes: | Code | Description | |------|-------------| | `ERR_FIELD_REQUIRED` | A required field is missing. | | `ERR_INVALID_TIMESTAMP` | Timestamp is not a valid ISO 8601 date or is not in the future. | | `ERR_INVALID_FIELD_TYPE` | A field has an invalid type. | | `ERR_CONFLICT_ID_EXISTS` | The `campaign_id` value is already in use. | | `ERR_SKU_UNSUPPORTED_FOR_RETAILER` | SKU-based list items are not supported for this retailer. | ---