├── .editorconfig ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── advanced-atjs-integration-serverstate ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── AppMeasurement.js │ ├── VisitorAPI.js │ └── at.js ├── server.js └── templates │ └── index.tpl ├── ecid-analytics-atjs-integration ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── AppMeasurement.js │ ├── VisitorAPI.js │ └── at.js ├── server.js └── templates │ └── index.tpl ├── ecid-analytics-integration ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── AppMeasurement.js │ └── VisitorAPI.js ├── server.js └── templates │ └── index.tpl ├── ecid-customer-ids-integration ├── README.md ├── package-lock.json ├── package.json ├── public │ └── VisitorAPI.js ├── server.js └── templates │ └── index.tpl ├── ecid-integration ├── README.md ├── package-lock.json ├── package.json ├── public │ └── VisitorAPI.js ├── server.js └── templates │ └── index.tpl ├── feature-flag ├── README.md ├── index.handlebars ├── package-lock.json ├── package.json ├── provider.js ├── public │ ├── demo-marketing-offer1-exp-A.png │ ├── demo-marketing-offer1-exp-B.png │ └── style.css ├── sampleRules.json └── server.js ├── multiple-mbox-ecid-analytics-atjs-integration ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── AppMeasurement.js │ ├── VisitorAPI.js │ └── at.js ├── server.js └── templates │ └── index.tpl ├── next-server-side-rendering-demo ├── .gitignore ├── README.md ├── components │ ├── Header.js │ └── MyLayout.js ├── helpers │ ├── target-client-side.js │ ├── target-config.json │ └── target-server-side.js ├── package-lock.json ├── package.json ├── pages │ ├── _document.js │ ├── about.js │ ├── index.js │ └── p │ │ └── [id].js └── static │ ├── AppMeasurement.js │ ├── VisitorAPI.js │ └── at.js ├── on-device-decisioning ├── README.md ├── index.handlebars ├── package-lock.json ├── package.json ├── public │ ├── demo-marketing-offer1-exp-A.png │ ├── demo-marketing-offer1-exp-B.png │ └── style.css ├── sampleRules.json └── server.js ├── proxy-configuration ├── README.md ├── example-proxy-server.js ├── package-lock.json ├── package.json ├── proxy-sample-node-14.js ├── proxy-sample-node-16.js └── proxy-sample-node-18.js ├── react-shopping-cart-demo ├── .babelrc ├── .gitignore ├── README.md ├── config.json ├── index.tpl ├── package-lock.json ├── package.json ├── public │ ├── AppMeasurement.js │ ├── VisitorAPI.js │ ├── assets │ │ ├── css │ │ │ ├── app.css │ │ │ ├── app.css.map │ │ │ └── base.min.css │ │ ├── js │ │ │ └── app.js │ │ └── resources │ │ │ ├── data │ │ │ ├── latestProducts.json │ │ │ └── products.json │ │ │ └── images │ │ │ ├── carousel │ │ │ ├── bigsale.png │ │ │ ├── discount.png │ │ │ ├── easter.png │ │ │ ├── family.png │ │ │ ├── happy.png │ │ │ └── percent.png │ │ │ ├── logo.png │ │ │ ├── poc.png │ │ │ ├── products │ │ │ ├── baby.png │ │ │ ├── bag.jpeg │ │ │ ├── floraldress.png │ │ │ ├── headphone.png │ │ │ ├── luggage.png │ │ │ ├── moisturizer.png │ │ │ ├── perfume.png │ │ │ ├── shoes.png │ │ │ ├── towels.png │ │ │ ├── tshirt.png │ │ │ └── watch.png │ │ │ └── target200.png │ └── at.js ├── server.js ├── src │ ├── actions │ │ ├── addToCart.js │ │ ├── addToWishlist.js │ │ ├── deleteCart.js │ │ ├── fetchAbout.js │ │ ├── fetchCart.js │ │ ├── fetchLatestProducts.js │ │ ├── fetchProduct.js │ │ ├── fetchProducts.js │ │ ├── fetchWishlist.js │ │ ├── removeFromCart.js │ │ └── removeFromWishlist.js │ ├── components │ │ ├── CartItem.jsx │ │ ├── Footer.jsx │ │ ├── ProductItem.jsx │ │ └── WishlistItem.jsx │ ├── containers │ │ ├── About.jsx │ │ ├── App.jsx │ │ ├── Cart.jsx │ │ ├── Checkout.js │ │ ├── Confirm.js │ │ ├── Home.jsx │ │ ├── Navbar.jsx │ │ ├── Products.js │ │ ├── SingleProduct.jsx │ │ └── Wishlist.jsx │ ├── index.js │ ├── middlewares │ │ ├── Auth.js │ │ └── Loading.js │ ├── reducers │ │ ├── AboutReducer.js │ │ ├── CartReducer.js │ │ ├── LatestProductsReducer.js │ │ ├── LoadingReducer.js │ │ ├── ProductReducer.js │ │ ├── ProductsReducer.js │ │ ├── WishlistReducer.js │ │ └── index.js │ ├── store.js │ ├── styles │ │ └── sass │ │ │ └── app.scss │ └── utils │ │ ├── customEvents.js │ │ └── mockAxios.js └── webpack.config.js ├── shared-ecid-analytics-integration ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── AppMeasurement.js │ └── VisitorAPI.js ├── server.js └── templates │ └── index.tpl ├── target-only ├── README.md ├── package-lock.json ├── package.json ├── server.js └── templates │ └── index.tpl └── target-traces ├── README.md ├── package-lock.json ├── package.json ├── server.js └── templates └── index.tpl /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for choosing to contribute! 4 | 5 | The following are a set of guidelines to follow when contributing to this project. 6 | 7 | ## Code Of Conduct 8 | 9 | This project adheres to the Adobe [code of conduct](../CODE_OF_CONDUCT.md). By participating, 10 | you are expected to uphold this code. Please report unacceptable behavior to 11 | [Grp-opensourceoffice@adobe.com](mailto:Grp-opensourceoffice@adobe.com). 12 | 13 | ## Have A Question? 14 | 15 | Start by filing an issue. The existing committers on this project work to reach 16 | consensus around project direction and issue solutions within issue threads 17 | (when appropriate). 18 | 19 | ## Contributor License Agreement 20 | 21 | All third-party contributions to this project must be accompanied by a signed contributor 22 | license agreement. This gives Adobe permission to redistribute your contributions 23 | as part of the project. [Sign our CLA](https://opensource.adobe.com/cla.html). You 24 | only need to submit an Adobe CLA one time, so if you have submitted one previously, 25 | you are good to go! 26 | 27 | ## Code Reviews 28 | 29 | All submissions should come in the form of pull requests and need to be reviewed 30 | by project committers. Read [GitHub's pull request documentation](https://help.github.com/articles/about-pull-requests/) 31 | for more information on sending pull requests. 32 | 33 | Lastly, please follow the [pull request template](PULL_REQUEST_TEMPLATE.md) when 34 | submitting a pull request! 35 | 36 | ## From Contributor To Committer 37 | 38 | We love contributions from our community! If you'd like to go a step beyond contributor 39 | and become a committer with full write access and a say in the project, you must 40 | be invited to the project. The existing committers employ an internal nomination 41 | process that must reach lazy consensus (silence is approval) before invitations 42 | are issued. If you feel you are qualified and want to get more deeply involved, 43 | feel free to reach out to existing committers to have a conversation about that. 44 | 45 | ## Security Issues 46 | 47 | Security issues shouldn't be reported on this issue tracker. Instead, [file an issue to our security experts](https://helpx.adobe.com/security/alertus.html) 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ### Expected Behaviour 5 | 6 | ### Actual Behaviour 7 | 8 | ### Reproduce Scenario (including but not limited to) 9 | 10 | #### Steps to Reproduce 11 | 12 | #### Platform and Version 13 | 14 | #### Sample Code that illustrates the problem 15 | 16 | #### Logs taken while reproducing problem 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | 7 | ## Related Issue 8 | 9 | 10 | 11 | 12 | 13 | 14 | ## Motivation and Context 15 | 16 | 17 | 18 | ## How Has This Been Tested? 19 | 20 | 21 | 22 | 23 | 24 | ## Screenshots (if appropriate): 25 | 26 | ## Types of changes 27 | 28 | 29 | 30 | - [ ] Bug fix (non-breaking change which fixes an issue) 31 | - [ ] New feature (non-breaking change which adds functionality) 32 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 33 | 34 | ## Checklist: 35 | 36 | 37 | 38 | 39 | - [ ] I have signed the [Adobe Open Source CLA](https://opensource.adobe.com/cla.html). 40 | - [ ] My code follows the code style of this project. 41 | - [ ] My change requires a change to the documentation. 42 | - [ ] I have updated the documentation accordingly. 43 | - [ ] I have read the **CONTRIBUTING** document. 44 | - [ ] I have added tests to cover my changes. 45 | - [ ] All new and existing tests passed. 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | .idea 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Adobe Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at Grp-opensourceoffice@adobe.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [https://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: https://contributor-covenant.org 74 | [version]: https://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Target Node.js SDK Samples & Demos 2 | 3 | This repository contains samples and demos for the [Target Node.js SDK](https://www.npmjs.com/package/@adobe/target-nodejs-sdk) 4 | 5 | ## Demos 6 | 7 | - [react-shopping-cart-demo](react-shopping-cart-demo) - a demo showing how to fetch and inject Target offers in a 8 | [React Redux](https://react-redux.js.org/) app on the server side and then instantly apply the offers on the client side, 9 | without any additional client-side Target calls. 10 | - [next-server-side-rendering-demo](next-server-side-rendering-demo) - a demo showing how to fetch and inject Target offers 11 | in a [Next.js](https://nextjs.org/) server-side rendered app, and then instantly apply the offers on the client side, 12 | without any additional client-side Target calls. 13 | 14 | ## Samples 15 | 16 | - [target-only](target-only) - shows how Target Node.js SDK can be used in isolation 17 | - [ecid-integration](ecid-integration) - shows how Target Node.js SDK can be used in conjunction with ECID (Visitor API) 18 | - [ecid-customer-ids-integration](ecid-customer-ids-integration) - shows how Target Node.js SDK can be used in 19 | conjunction with ECID (Visitor API) and Customer IDs, useful for tracking user authentication. 20 | - [ecid-analytics-integration](ecid-analytics-integration) - shows how Target Node.js SDK can be used in conjunction 21 | with ECID (Visitor API) and Adobe Analytics. 22 | - [ecid-analytics-atjs-integration](ecid-analytics-atjs-integration) - shows how Target Node.js SDK can be used in 23 | conjunction with ECID (Visitor API), Adobe Analytics and at.js. This sample shows how to run testing in "hybrid" mode, 24 | when the test is started on the server side and then it is handed off to at.js on the client side. 25 | - [advanced-atjs-integration-serverstate](advanced-atjs-integration-serverstate) - shows how to use at.js v2.2+ **serverState** feature to apply Target offers fetched on the server side, without any additional client side content-fetching Target calls. 26 | - [target-traces](target-traces) - shows how Target Node.js SDK can be used to retrieve Target execution traces. 27 | - [shared-ecid-analytics-integration](shared-ecid-analytics-integration) - shows how to properly handle multiple Target 28 | Node.js SDK API calls when processing a client request, sharing the same ECID instance. 29 | - [multiple-mbox-ecid-analytics-atjs-integration](multiple-mbox-ecid-analytics-atjs-integration) - shows how Target 30 | Node.js SDK can be used to request content for multiple mboxes in the same Target call. 31 | - [on-device-decisioning](on-device-decisioning) - shows how Target Node.js SDK can be used in on-device decisioning method 32 | - [feature-flag](feature-flag) - shows how Target Node.js SDK can be easily used for feature flags 33 | 34 | For Target Node.js SDK documentation, see [Target Node.js SDK NPM page](https://www.npmjs.com/package/@adobe/target-nodejs-sdk). 35 | 36 | ## Contributing 37 | 38 | Check out our [Contribution guidelines](.github/CONTRIBUTING.md) as well as [Code of Conduct](CODE_OF_CONDUCT.md) prior 39 | to contributing to Target Node.js SDK samples. 40 | -------------------------------------------------------------------------------- /advanced-atjs-integration-serverstate/README.md: -------------------------------------------------------------------------------- 1 | # Advanced at.js integration via serverState sample 2 | 3 | In the `ecid-analytics-atjs-integration` sample, we've showcased "hybrid" Target integration, where both the server-side and the client-side Target libraries are hitting the same Target edge cluster, sharing the same Target session and visitor state. However, at.js still needs to go over the wire for fetching Target content in the browser, prehiding the whole page BODY until Target offers are fetched and applied. 4 | 5 | But what if we could prefetch the Target content on the server-side, include it in the page returned to the client, and then just have at.js apply the Target offers immediately, without making another expensive network call? 6 | Also, in this case at.js will be able to prehide only the specific DOM elements for which Target offers have been fetched on the server-side, thus no longer requiring the prehiding of the whole page BODY. 7 | 8 | Target `serverState` is a new feature available in at.js v2.2+, that allows at.js to apply Target offers directly from content fetched on the server side and returned to the client as part of the page being served. 9 | 10 | In order to use this feature with Target Node.js SDK we just have to set `window.targetGlobalSettings.serverState` object in the returned page, from Target Delivery API request and response objects available after a successfull `getOffers()` API call, as follows: 11 | 12 | ```js 13 | // First, we fetch the offers via Target Node.js SDK API, as usual 14 | const targetResponse = await targetClient.getOffers(options); 15 | // A successfull response will contain Target Delivery API request and response objects, which we need to set as serverState 16 | const serverState = { 17 | request: targetResponse.request, 18 | response: targetResponse.response 19 | }; 20 | // Finally, we should set window.targetGlobalSettings.serverState in the returned page, by replacing it in a page template, for example 21 | const PAGE_TEMPLATE = ` 22 | 23 | 24 | 25 | ... 26 | 32 | 33 | 34 | ... 35 | 36 | `; 37 | // Return PAGE_TEMPLATE to the client ... 38 | ``` 39 | 40 | Once the page is loaded in the browser, at.js will apply all the Target offers from `serverState` immediately, without firing any network calls against the Target edge. Additionally, at.js will only prehide the DOM elements for which Target offers are available in the content fetched server-side, thus greatly improving page load performance and end-user experience. 41 | 42 | Note: In case of SPAs using [Target Views and triggerView() at.js API](https://docs.adobe.com/content/help/en/target/using/implement-target/client-side/functions-overview/adobe-target-triggerview-atjs-2.html), at.js will cache the content for all Views prefetched on the server-side and apply these as soon as each View is triggered via `triggerView()`, again without firing any additional content-fetching calls to Target. 43 | 44 | ## Usage 45 | 1. Install dependencies: `npm i` 46 | 2. Start: `npm start` 47 | -------------------------------------------------------------------------------- /advanced-atjs-integration-serverstate/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "advanced-atjs-integration-serverstate", 3 | "version": "1.0.0", 4 | "description": "Adobe Target Node.js SDK, advanced-atjs-integration-serverstate sample", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:adobe/target-nodejs-sdk-samples.git" 12 | }, 13 | "keywords": [ 14 | "NodeJS", 15 | "Server", 16 | "API", 17 | "Adobe", 18 | "Target", 19 | "MCID", 20 | "ECID", 21 | "Visitor", 22 | "Delivery", 23 | "serverState", 24 | "advanced-atjs-integration-serverstate" 25 | ], 26 | "author": "Adobe Systems Inc.", 27 | "license": "Apache-2.0", 28 | "dependencies": { 29 | "@adobe/target-nodejs-sdk": "^2.1.0", 30 | "cookie-parser": "^1.4.4", 31 | "express": "^4.17.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /advanced-atjs-integration-serverstate/server.js: -------------------------------------------------------------------------------- 1 | /*************************************************************************************** 2 | * (c) 2019 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | ****************************************************************************************/ 12 | 13 | const fs = require("fs"); 14 | const express = require("express"); 15 | const cookieParser = require("cookie-parser"); 16 | const TargetClient = require("@adobe/target-nodejs-sdk"); 17 | const CONFIG = { 18 | client: "adobetargetmobile", 19 | organizationId: "B8A054D958807F770A495DD6@AdobeOrg", 20 | timeout: 10000, 21 | logger: console 22 | }; 23 | const targetClient = TargetClient.create(CONFIG); 24 | const TEMPLATE = fs.readFileSync(__dirname + "/templates/index.tpl").toString(); 25 | 26 | const app = express(); 27 | app.use(cookieParser()); 28 | app.use(express.static(__dirname + "/public")); 29 | 30 | function saveCookie(res, cookie) { 31 | if (!cookie) { 32 | return; 33 | } 34 | 35 | res.cookie(cookie.name, cookie.value, { maxAge: cookie.maxAge * 1000 }); 36 | } 37 | 38 | const getResponseHeaders = () => ({ 39 | "Content-Type": "text/html", 40 | Expires: new Date().toUTCString() 41 | }); 42 | 43 | function sendHtml(res, targetResponse) { 44 | const serverState = { 45 | request: targetResponse.request, 46 | response: targetResponse.response 47 | }; 48 | const htmlResponse = TEMPLATE.replace( 49 | "${organizationId}", 50 | CONFIG.organizationId 51 | ) 52 | .replace("${visitorState}", JSON.stringify(targetResponse.visitorState)) 53 | .replace("${serverState}", JSON.stringify(serverState, null, " ")) 54 | .replace("${content}", JSON.stringify(targetResponse, null, " ")); 55 | 56 | res.status(200).send(htmlResponse); 57 | } 58 | 59 | function sendSuccessResponse(res, response) { 60 | res.set(getResponseHeaders()); 61 | saveCookie(res, response.targetCookie); 62 | saveCookie(res, response.targetLocationHintCookie); 63 | sendHtml(res, response); 64 | } 65 | 66 | function sendErrorResponse(res, error) { 67 | res.set(getResponseHeaders()); 68 | res.status(500).send(error); 69 | } 70 | 71 | function getAddress(req) { 72 | return { url: req.headers.host + req.originalUrl }; 73 | } 74 | 75 | app.get("/", async (req, res) => { 76 | const visitorCookie = 77 | req.cookies[ 78 | encodeURIComponent( 79 | TargetClient.getVisitorCookieName(CONFIG.organizationId) 80 | ) 81 | ]; 82 | const targetCookie = req.cookies[TargetClient.TargetCookieName]; 83 | const targetLocationHintCookie = 84 | req.cookies[TargetClient.TargetLocationHintCookieName]; 85 | const request = { 86 | execute: { 87 | mboxes: [ 88 | { 89 | address: getAddress(req), 90 | name: "a1-serverside-ab" 91 | } 92 | ] 93 | } 94 | }; 95 | 96 | try { 97 | const response = await targetClient.getOffers({ 98 | request, 99 | visitorCookie, 100 | targetCookie, 101 | targetLocationHintCookie 102 | }); 103 | sendSuccessResponse(res, response); 104 | } catch (error) { 105 | console.error("Target:", error); 106 | sendErrorResponse(res, error); 107 | } 108 | }); 109 | 110 | app.listen(3000, function() { 111 | console.log("Listening on port 3000 and watching!"); 112 | }); 113 | -------------------------------------------------------------------------------- /advanced-atjs-integration-serverstate/templates/index.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Advanced at.js Integration with ServerState Sample 6 | 7 | 10 | 16 | 17 | 18 | 19 |
${content}
20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /ecid-analytics-atjs-integration/README.md: -------------------------------------------------------------------------------- 1 | # ECID, Analytics and at.js integration sample 2 | 3 | Most of the time Target Node.js SDK will be used in a NodeJS application, such as `Express`, `Hapi`, `Koa`, etc. 4 | However, with the recent proliferation of SPA frameworks that allow server-side rendering (such as Facebook React, 5 | Next.js or Angular), there are use cases where server-side code should work in tandem with client-side libraries. 6 | In Target's case the client-side library is `at.js`. 7 | The integration between server-side and client-side is also known as "hybrid" testing mode. 8 | The biggest challenge in this case is ensuring that both server-side and client-side Target calls are hitting the same 9 | Target edge cluster. Otherwise, one may end up with different user profiles being created by server-side and client-side 10 | calls for the same visitor. 11 | 12 | To solve this, Target leverages the so-called "location hint" cookie. To be able to use the location hint cookie, the 13 | following JavaScript snippet must be added to the rendered page before `at.js` (or before the Target Adobe Launch extension 14 | is initialized when Adobe Launch tag manager is used): 15 | 16 | ```js 17 | window.targetGlobalSettings = { 18 | overrideMboxEdgeServer: true 19 | }; 20 | ``` 21 | 22 | ## Usage 23 | 1. Install dependencies: `npm i` 24 | 2. Start: `npm start` 25 | -------------------------------------------------------------------------------- /ecid-analytics-atjs-integration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecid-analytics-atjs-integration", 3 | "version": "1.0.0", 4 | "description": "Adobe Target Node.js SDK, ecid-analytics-atjs-integration sample", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:adobe/target-nodejs-sdk-samples.git" 12 | }, 13 | "keywords": [ 14 | "NodeJS", 15 | "Server", 16 | "API", 17 | "Adobe", 18 | "Target", 19 | "MCID", 20 | "ECID", 21 | "Visitor", 22 | "Delivery", 23 | "ecid-analytics-atjs-integration" 24 | ], 25 | "author": "Adobe Systems Inc.", 26 | "license": "Apache-2.0", 27 | "dependencies": { 28 | "@adobe/target-nodejs-sdk": "^2.1.0", 29 | "cookie-parser": "^1.4.4", 30 | "express": "^4.17.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ecid-analytics-atjs-integration/server.js: -------------------------------------------------------------------------------- 1 | /*************************************************************************************** 2 | * (c) 2019 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | ****************************************************************************************/ 12 | 13 | const fs = require("fs"); 14 | const express = require("express"); 15 | const cookieParser = require("cookie-parser"); 16 | const TargetClient = require("@adobe/target-nodejs-sdk"); 17 | const CONFIG = { 18 | client: "adobetargetmobile", 19 | organizationId: "B8A054D958807F770A495DD6@AdobeOrg", 20 | timeout: 10000, 21 | logger: console 22 | }; 23 | const targetClient = TargetClient.create(CONFIG); 24 | const TEMPLATE = fs.readFileSync(__dirname + "/templates/index.tpl").toString(); 25 | 26 | const app = express(); 27 | app.use(cookieParser()); 28 | app.use(express.static(__dirname + "/public")); 29 | 30 | function saveCookie(res, cookie) { 31 | if (!cookie) { 32 | return; 33 | } 34 | 35 | res.cookie(cookie.name, cookie.value, { maxAge: cookie.maxAge * 1000 }); 36 | } 37 | 38 | const getResponseHeaders = () => ({ 39 | "Content-Type": "text/html", 40 | Expires: new Date().toUTCString() 41 | }); 42 | 43 | function sendHtml(res, offer) { 44 | const htmlResponse = TEMPLATE.replace( 45 | "${organizationId}", 46 | CONFIG.organizationId 47 | ) 48 | .replace("${visitorState}", JSON.stringify(offer.visitorState)) 49 | .replace("${content}", JSON.stringify(offer, null, " ")); 50 | 51 | res.status(200).send(htmlResponse); 52 | } 53 | 54 | function sendSuccessResponse(res, response) { 55 | res.set(getResponseHeaders()); 56 | saveCookie(res, response.targetCookie); 57 | saveCookie(res, response.targetLocationHintCookie); 58 | sendHtml(res, response); 59 | } 60 | 61 | function sendErrorResponse(res, error) { 62 | res.set(getResponseHeaders()); 63 | res.status(500).send(error); 64 | } 65 | 66 | function getAddress(req) { 67 | return { url: req.headers.host + req.originalUrl }; 68 | } 69 | 70 | app.get("/", async (req, res) => { 71 | const visitorCookie = 72 | req.cookies[ 73 | encodeURIComponent( 74 | TargetClient.getVisitorCookieName(CONFIG.organizationId) 75 | ) 76 | ]; 77 | const targetCookie = req.cookies[TargetClient.TargetCookieName]; 78 | const targetLocationHintCookie = 79 | req.cookies[TargetClient.TargetLocationHintCookieName]; 80 | const request = { 81 | execute: { 82 | mboxes: [ 83 | { 84 | address: getAddress(req), 85 | name: "a1-serverside-ab" 86 | } 87 | ] 88 | } 89 | }; 90 | 91 | try { 92 | const response = await targetClient.getOffers({ 93 | request, 94 | visitorCookie, 95 | targetCookie, 96 | targetLocationHintCookie 97 | }); 98 | sendSuccessResponse(res, response); 99 | } catch (error) { 100 | console.error("Target:", error); 101 | sendErrorResponse(res, error); 102 | } 103 | }); 104 | 105 | app.listen(3000, function() { 106 | console.log("Listening on port 3000 and watching!"); 107 | }); 108 | -------------------------------------------------------------------------------- /ecid-analytics-atjs-integration/templates/index.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ECID (Visitor API) with Analytics and at.js Integration Sample 6 | 7 | 10 | 15 | 16 | 17 | 18 |
${content}
19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /ecid-analytics-integration/README.md: -------------------------------------------------------------------------------- 1 | # ECID and Analytics integration sample 2 | 3 | To get the most out of the Target Node.js SDK and to use the powerful analytics capabilities provided by Adobe Analytics, 4 | you can use the Target, ECID and Analytics combo. 5 | 6 | Using MCID, Analytics, and Target lets you: 7 | - Use segments from Adobe Audience Manager 8 | - Customize the user experience based on the content retrieved from Target 9 | - Ensure that all events and success metrics are collected in Analytics 10 | - Use Analytics' powerful queries and benefit from awesome report visualizations 11 | 12 | ## Usage 13 | 1. Install dependencies: `npm i` 14 | 2. Start: `npm start` 15 | -------------------------------------------------------------------------------- /ecid-analytics-integration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecid-analytics-integration", 3 | "version": "1.0.0", 4 | "description": "Adobe Target Node.js SDK, ecid-analytics-integration sample", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:adobe/target-nodejs-sdk-samples.git" 12 | }, 13 | "keywords": [ 14 | "NodeJS", 15 | "Server", 16 | "API", 17 | "Adobe", 18 | "Target", 19 | "MCID", 20 | "ECID", 21 | "Visitor", 22 | "Delivery", 23 | "ecid-analytics-integration" 24 | ], 25 | "author": "Adobe Systems Inc.", 26 | "license": "Apache-2.0", 27 | "dependencies": { 28 | "@adobe/target-nodejs-sdk": "^2.1.0", 29 | "cookie-parser": "^1.4.4", 30 | "express": "^4.17.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ecid-analytics-integration/server.js: -------------------------------------------------------------------------------- 1 | /*************************************************************************************** 2 | * (c) 2019 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | ****************************************************************************************/ 12 | 13 | const fs = require("fs"); 14 | const express = require("express"); 15 | const cookieParser = require("cookie-parser"); 16 | const TargetClient = require("@adobe/target-nodejs-sdk"); 17 | const CONFIG = { 18 | client: "adobetargetmobile", 19 | organizationId: "B8A054D958807F770A495DD6@AdobeOrg", 20 | timeout: 10000, 21 | logger: console 22 | }; 23 | const targetClient = TargetClient.create(CONFIG); 24 | const TEMPLATE = fs.readFileSync(__dirname + "/templates/index.tpl").toString(); 25 | 26 | const app = express(); 27 | app.use(cookieParser()); 28 | app.use(express.static(__dirname + "/public")); 29 | 30 | function saveCookie(res, cookie) { 31 | if (!cookie) { 32 | return; 33 | } 34 | 35 | res.cookie(cookie.name, cookie.value, { maxAge: cookie.maxAge * 1000 }); 36 | } 37 | 38 | const getResponseHeaders = () => ({ 39 | "Content-Type": "text/html", 40 | Expires: new Date().toUTCString() 41 | }); 42 | 43 | function sendHtml(res, offer) { 44 | const htmlResponse = TEMPLATE.replace( 45 | "${organizationId}", 46 | CONFIG.organizationId 47 | ) 48 | .replace("${visitorState}", JSON.stringify(offer.visitorState)) 49 | .replace("${content}", JSON.stringify(offer, null, " ")); 50 | 51 | res.status(200).send(htmlResponse); 52 | } 53 | 54 | function sendSuccessResponse(res, response) { 55 | res.set(getResponseHeaders()); 56 | saveCookie(res, response.targetCookie); 57 | sendHtml(res, response); 58 | } 59 | 60 | function sendErrorResponse(res, error) { 61 | res.set(getResponseHeaders()); 62 | res.status(500).send(error); 63 | } 64 | 65 | function getAddress(req) { 66 | return { url: req.headers.host + req.originalUrl }; 67 | } 68 | 69 | app.get("/", async (req, res) => { 70 | const visitorCookie = 71 | req.cookies[ 72 | encodeURIComponent( 73 | TargetClient.getVisitorCookieName(CONFIG.organizationId) 74 | ) 75 | ]; 76 | const targetCookie = req.cookies[TargetClient.TargetCookieName]; 77 | const request = { 78 | execute: { 79 | mboxes: [ 80 | { 81 | address: getAddress(req), 82 | name: "a1-serverside-ab" 83 | } 84 | ] 85 | } 86 | }; 87 | 88 | try { 89 | const response = await targetClient.getOffers({ 90 | request, 91 | visitorCookie, 92 | targetCookie 93 | }); 94 | sendSuccessResponse(res, response); 95 | } catch (error) { 96 | console.error("Target:", error); 97 | sendErrorResponse(res, error); 98 | } 99 | }); 100 | 101 | app.listen(3000, function() { 102 | console.log("Listening on port 3000 and watching!"); 103 | }); 104 | -------------------------------------------------------------------------------- /ecid-analytics-integration/templates/index.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ECID (Visitor API) with Analytics Integration Sample 6 | 7 | 10 | 11 | 12 |
${content}
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /ecid-customer-ids-integration/README.md: -------------------------------------------------------------------------------- 1 | # ECID with Customer Ids integration sample 2 | 3 | In order to track visitor user accounts and logon status details, `customerIds` may be passed to Target. 4 | The `customerIds` object is similar to the ECID functionality described here: https://docs.adobe.com/content/help/en/id-service/using/reference/authenticated-state.html 5 | 6 | ## Usage 7 | 1. Install dependencies: `npm i` 8 | 2. Start: `npm start` 9 | -------------------------------------------------------------------------------- /ecid-customer-ids-integration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecid-customer-ids-integration", 3 | "version": "1.0.0", 4 | "description": "Adobe Target Node.js SDK, ecid-customer-ids-integration sample", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:adobe/target-nodejs-sdk-samples.git" 12 | }, 13 | "keywords": [ 14 | "NodeJS", 15 | "Server", 16 | "API", 17 | "Adobe", 18 | "Target", 19 | "MCID", 20 | "ECID", 21 | "Visitor", 22 | "Delivery", 23 | "ecid-customer-ids-integration" 24 | ], 25 | "author": "Adobe Systems Inc.", 26 | "license": "Apache-2.0", 27 | "dependencies": { 28 | "@adobe/target-nodejs-sdk": "^2.1.0", 29 | "cookie-parser": "^1.4.4", 30 | "express": "^4.17.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ecid-customer-ids-integration/server.js: -------------------------------------------------------------------------------- 1 | /*************************************************************************************** 2 | * (c) 2019 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | ****************************************************************************************/ 12 | 13 | const fs = require("fs"); 14 | const express = require("express"); 15 | const cookieParser = require("cookie-parser"); 16 | const TargetClient = require("@adobe/target-nodejs-sdk"); 17 | const CONFIG = { 18 | client: "adobetargetmobile", 19 | organizationId: "B8A054D958807F770A495DD6@AdobeOrg", 20 | timeout: 10000, 21 | logger: console 22 | }; 23 | const targetClient = TargetClient.create(CONFIG); 24 | const TEMPLATE = fs.readFileSync(__dirname + "/templates/index.tpl").toString(); 25 | 26 | const app = express(); 27 | app.use(cookieParser()); 28 | app.use(express.static(__dirname + "/public")); 29 | 30 | function saveCookie(res, cookie) { 31 | if (!cookie) { 32 | return; 33 | } 34 | 35 | res.cookie(cookie.name, cookie.value, { maxAge: cookie.maxAge * 1000 }); 36 | } 37 | 38 | const getResponseHeaders = () => ({ 39 | "Content-Type": "text/html", 40 | Expires: new Date().toUTCString() 41 | }); 42 | 43 | function sendHtml(res, offer) { 44 | const htmlResponse = TEMPLATE.replace( 45 | "${organizationId}", 46 | CONFIG.organizationId 47 | ) 48 | .replace("${visitorState}", JSON.stringify(offer.visitorState)) 49 | .replace("${content}", JSON.stringify(offer, null, " ")); 50 | 51 | res.status(200).send(htmlResponse); 52 | } 53 | 54 | function sendSuccessResponse(res, response) { 55 | res.set(getResponseHeaders()); 56 | saveCookie(res, response.targetCookie); 57 | sendHtml(res, response); 58 | } 59 | 60 | function sendErrorResponse(res, error) { 61 | res.set(getResponseHeaders()); 62 | res.status(500).send(error); 63 | } 64 | 65 | function getAddress(req) { 66 | return { url: req.headers.host + req.originalUrl }; 67 | } 68 | 69 | app.get("/", async (req, res) => { 70 | const visitorCookie = 71 | req.cookies[ 72 | encodeURIComponent( 73 | TargetClient.getVisitorCookieName(CONFIG.organizationId) 74 | ) 75 | ]; 76 | const targetCookie = req.cookies[TargetClient.TargetCookieName]; 77 | const customerIds = { 78 | userid: { 79 | id: "67312378756723456", 80 | authState: TargetClient.AuthState.AUTHENTICATED 81 | } 82 | }; 83 | const request = { 84 | execute: { 85 | mboxes: [ 86 | { 87 | address: getAddress(req), 88 | name: "a1-serverside-ab" 89 | } 90 | ] 91 | } 92 | }; 93 | 94 | try { 95 | const response = await targetClient.getOffers({ 96 | request, 97 | visitorCookie, 98 | targetCookie, 99 | customerIds 100 | }); 101 | sendSuccessResponse(res, response); 102 | } catch (error) { 103 | console.error("Target:", error); 104 | sendErrorResponse(res, error); 105 | } 106 | }); 107 | 108 | app.listen(3000, function() { 109 | console.log("Listening on port 3000 and watching!"); 110 | }); 111 | -------------------------------------------------------------------------------- /ecid-customer-ids-integration/templates/index.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ECID (Visitor API) with Customer IDs Integration Sample 6 | 7 | 10 | 11 | 12 |
${content}
13 | 14 | 15 | -------------------------------------------------------------------------------- /ecid-integration/README.md: -------------------------------------------------------------------------------- 1 | # ECID integration sample 2 | 3 | Although using the Target Node.js SDK for fetching content from Target can be powerful, the added value of using ECID 4 | for user tracking outweighs using Target only. ECID allows leveraging all the cool features of the Adobe Experience Cloud, 5 | such as audience sharing, analytics integration, etc. 6 | Using Target and ECID in an `Express` application is pretty straightforward. ECID has a client-side part, so we'll have 7 | to use a simple template that references the ECID client-side JavaScript library. 8 | 9 | ## Usage 10 | 1. Install dependencies: `npm i` 11 | 2. Start: `npm start` 12 | -------------------------------------------------------------------------------- /ecid-integration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecid-integration", 3 | "version": "1.0.0", 4 | "description": "Adobe Target Node.js SDK, ecid-integration sample", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:adobe/target-nodejs-sdk-samples.git" 12 | }, 13 | "keywords": [ 14 | "NodeJS", 15 | "Server", 16 | "API", 17 | "Adobe", 18 | "Target", 19 | "MCID", 20 | "ECID", 21 | "Visitor", 22 | "Delivery", 23 | "ecid-integration" 24 | ], 25 | "author": "Adobe Systems Inc.", 26 | "license": "Apache-2.0", 27 | "dependencies": { 28 | "@adobe/target-nodejs-sdk": "^2.1.0", 29 | "cookie-parser": "^1.4.4", 30 | "express": "^4.17.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ecid-integration/server.js: -------------------------------------------------------------------------------- 1 | /*************************************************************************************** 2 | * (c) 2019 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | ****************************************************************************************/ 12 | 13 | const fs = require("fs"); 14 | const express = require("express"); 15 | const cookieParser = require("cookie-parser"); 16 | const TargetClient = require("@adobe/target-nodejs-sdk"); 17 | const CONFIG = { 18 | client: "adobetargetmobile", 19 | organizationId: "B8A054D958807F770A495DD6@AdobeOrg", 20 | timeout: 10000, 21 | logger: console 22 | }; 23 | const targetClient = TargetClient.create(CONFIG); 24 | const TEMPLATE = fs.readFileSync(__dirname + "/templates/index.tpl").toString(); 25 | 26 | const app = express(); 27 | app.use(cookieParser()); 28 | app.use(express.static(__dirname + "/public")); 29 | 30 | function saveCookie(res, cookie) { 31 | if (!cookie) { 32 | return; 33 | } 34 | 35 | res.cookie(cookie.name, cookie.value, { maxAge: cookie.maxAge * 1000 }); 36 | } 37 | 38 | const getResponseHeaders = () => ({ 39 | "Content-Type": "text/html", 40 | Expires: new Date().toUTCString() 41 | }); 42 | 43 | function sendHtml(res, offer) { 44 | const htmlResponse = TEMPLATE.replace( 45 | "${organizationId}", 46 | CONFIG.organizationId 47 | ) 48 | .replace("${visitorState}", JSON.stringify(offer.visitorState)) 49 | .replace("${content}", JSON.stringify(offer, null, " ")); 50 | 51 | res.status(200).send(htmlResponse); 52 | } 53 | 54 | function sendSuccessResponse(res, response) { 55 | res.set(getResponseHeaders()); 56 | saveCookie(res, response.targetCookie); 57 | sendHtml(res, response); 58 | } 59 | 60 | function sendErrorResponse(res, error) { 61 | res.set(getResponseHeaders()); 62 | res.status(500).send(error); 63 | } 64 | 65 | function getAddress(req) { 66 | return { url: req.headers.host + req.originalUrl }; 67 | } 68 | 69 | app.get("/", async (req, res) => { 70 | const visitorCookie = 71 | req.cookies[ 72 | encodeURIComponent( 73 | TargetClient.getVisitorCookieName(CONFIG.organizationId) 74 | ) 75 | ]; 76 | const targetCookie = req.cookies[TargetClient.TargetCookieName]; 77 | const request = { 78 | execute: { 79 | mboxes: [ 80 | { 81 | address: getAddress(req), 82 | name: "a1-serverside-ab" 83 | } 84 | ] 85 | } 86 | }; 87 | 88 | try { 89 | const response = await targetClient.getOffers({ 90 | request, 91 | visitorCookie, 92 | targetCookie 93 | }); 94 | sendSuccessResponse(res, response); 95 | } catch (error) { 96 | console.error("Target:", error); 97 | sendErrorResponse(res, error); 98 | } 99 | }); 100 | 101 | app.listen(3000, function() { 102 | console.log("Listening on port 3000 and watching!"); 103 | }); 104 | -------------------------------------------------------------------------------- /ecid-integration/templates/index.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ECID (Visitor API) Integration Sample 6 | 7 | 10 | 11 | 12 |
${content}
13 | 14 | 15 | -------------------------------------------------------------------------------- /feature-flag/README.md: -------------------------------------------------------------------------------- 1 | # Target feature flag sample 2 | 3 | ## Overview 4 | 5 | For this sample, we first created two simple AB activities. One to represent feature flags for engineering purposes and another for marketing purposes. Each activity has two experiences that have JSON offer content. The JSON holds unique key value pairs that are used by the sample app to determine what engineering systems to use and marketing content to show. 6 | 7 | ### Engineering Feature Flags Activity 8 | mbox: `demo-engineering-flags` 9 | 10 | #### Experience A 11 | ```json 12 | { 13 | "cdnHostname": "cdn.cloud.corp.net", 14 | "searchProviderId": "starwars", 15 | "hasLegacyAccess": false 16 | } 17 | ``` 18 | 19 | #### Experience B 20 | ```json 21 | { 22 | "cdnHostname": "cdn.megacloud.corp.com", 23 | "searchProviderId":"startrek", 24 | "hasLegacyAccess": true 25 | } 26 | ``` 27 | 28 | ### Marketing Activity 29 | mbox: `demo-marketing-offer1` 30 | 31 | ### Experience A 32 | ```json 33 | { 34 | "experience": "A", 35 | "asset": "demo-marketing-offer1-exp-A.png" 36 | } 37 | ``` 38 | ### Experience B 39 | 40 | ```json 41 | { 42 | "experience": "B", 43 | "asset": "demo-marketing-offer1-exp-B.png" 44 | } 45 | ``` 46 | 47 | When run, the sample app displays a marketing banner and a search box. The marketing banner is different depending on the `asset` value of the `demo-marketing-offer1` mbox. And the search experience differs depending on the `searchProviderId` value of the `demo-engineering-flags` mbox. If the value is `starwars`,a [Star Wars API](https://swapi.co/) is used to search for characters. If the value is `startrek`, a [Star Trek API](http://stapi.co/) is used to search for characters. 48 | 49 | In this sample, the `getAttributes` call is used to greatly simplify accessing the JSON offer. Typically, a developer would need to find the JSON offer object in the response of the `getOffers` call. This is done in other samples. It is straightforward, but can be cumbersome -- and it requires developers to be intimately familiar with the SDK response object. 50 | 51 | Instead, this sample uses the `getAttributes` call to get the offer instead. It then looks up the value of each attribute using one of the helper methods. 52 | 53 | ## Running the sample 54 | 1. Install dependencies: `npm i` 55 | 2. Start: `npm start` 56 | 3. Point a browser to http://127.0.0.1:3000 57 | 58 | 59 | ## How it works 60 | 61 | In the code sample below, take a look at the `getAttributes` call. An array of mbox names and an options object is passed in. The result is an attributes object with a few methods that can be used to get offer details. 62 | 63 | The `getValue` method is used to get the `searchProviderId` from the `demo-engineering-flags` mbox offer. 64 | 65 | And the `asObject` method is used to get a plain old JSON representation of the `demo-marketing-offer1` mbox offer. 66 | 67 | ```js 68 | const targetClient = TargetClient.create(CONFIG); 69 | const offerAttributes = await targetClient.getAttributes([ 70 | "demo-engineering-flags", 71 | "demo-marketing-offer1", 72 | ], { targetCookie }); 73 | 74 | 75 | //returns just the value of searchProviderId from the mbox offer 76 | const searchProviderId = offerAttributes.getValue("demo-engineering-flags", "searchProviderId"); 77 | 78 | //returns a simple JSON object representing the mbox offer 79 | const marketingOffer = offerAttributes.asObject("demo-marketing-offer1"); 80 | 81 | // the value of marketingOffer looks like this 82 | // { 83 | // "experience": "A", 84 | // "asset": "demo-marketing-offer1-exp-A.png" 85 | // } 86 | 87 | ``` 88 | 89 | Note: This sample uses on-device decisioning method. But the `getAttributes` method can be used in any decisioning method. 90 | -------------------------------------------------------------------------------- /feature-flag/index.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{pageTitle}} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | {{#if offer}} 15 |
16 |

{{pageTitle}}

17 |

Experience {{offer.experience}}

18 | 19 |
20 | {{/if}} 21 | 22 |
23 |

Search

24 |
25 |
26 | 27 |
28 | 29 |
30 | {{#if search.result}} 31 |

{{search.result.message}}

32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | {{#if search.result.list.length }} 42 | {{#each search.result.list}} 43 | 44 | 45 | 46 | 47 | 48 | {{/each}} 49 | {{else}} 50 | 51 | {{/if}} 52 | 53 |
NameGenderBirth Year
{{this.name}}{{this.gender}}{{this.birth_year}}
No results.
54 | {{/if}} 55 |
56 | 57 |
58 |

Flags

59 |
{{flags}}
60 |
61 | 62 | 66 |
67 | 68 | 69 | -------------------------------------------------------------------------------- /feature-flag/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "target-on-device-decisioning", 3 | "version": "1.0.0", 4 | "description": "Adobe Target Node.js SDK, target on-device decisioning sample", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node server.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:adobe/target-nodejs-sdk-samples.git" 12 | }, 13 | "keywords": [ 14 | "NodeJS", 15 | "Server", 16 | "API", 17 | "Adobe", 18 | "Target", 19 | "MCID", 20 | "ECID", 21 | "Visitor", 22 | "Delivery", 23 | "target-on-device-decisioning" 24 | ], 25 | "author": "Adobe Systems Inc.", 26 | "license": "Apache-2.0", 27 | "dependencies": { 28 | "@adobe/target-nodejs-sdk": "^2.1.0", 29 | "cookie-parser": "^1.4.5", 30 | "express": "^4.17.1", 31 | "handlebars": "^4.7.4", 32 | "lodash": "^4.17.15", 33 | "node-fetch": "^2.6.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /feature-flag/provider.js: -------------------------------------------------------------------------------- 1 | const fetch = require("node-fetch"); 2 | 3 | const starWarsSearchProvider = { 4 | domain: 'a Star Wars character ( try "skywalker")', 5 | execute: async (searchTerm) => { 6 | const result = await fetch( 7 | `https://swapi.dev/api/people/?format=json&search=${searchTerm}` 8 | ).then((result) => result.json()); 9 | 10 | const list = result.results; 11 | 12 | return { 13 | term: searchTerm, 14 | message: `Searched for ${searchTerm} with Star Wars search provider`, 15 | list, 16 | }; 17 | }, 18 | }; 19 | 20 | const starTrekSearchProvider = { 21 | domain: 'a Star Trek character ( try "picard")', 22 | execute: async (searchTerm) => { 23 | const searchParams = new URLSearchParams(); 24 | searchParams.set("title", searchTerm); 25 | searchParams.set("name", searchTerm); 26 | 27 | const result = await fetch(`http://stapi.co/api/v1/rest/character/search`, { 28 | method: "POST", 29 | body: searchParams, 30 | }).then((result) => result.json()); 31 | 32 | const list = (result.characters || []).map((character) => { 33 | return { 34 | name: character.name, 35 | gender: character.gender, 36 | birth_year: character.yearOfBirth, 37 | }; 38 | }); 39 | 40 | return { 41 | term: searchTerm, 42 | message: `Searched for ${searchTerm} with Star Trek search provider`, 43 | list, 44 | }; 45 | }, 46 | }; 47 | 48 | const searchProviders = { 49 | startrek: starTrekSearchProvider, 50 | starwars: starWarsSearchProvider, 51 | }; 52 | 53 | function getSearchProvider(providerId) { 54 | return searchProviders[providerId]; 55 | } 56 | 57 | module.exports = { getSearchProvider }; 58 | -------------------------------------------------------------------------------- /feature-flag/public/demo-marketing-offer1-exp-A.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/target-nodejs-sdk-samples/36c6921e6479202aad1ed437a104b8713fd939f7/feature-flag/public/demo-marketing-offer1-exp-A.png -------------------------------------------------------------------------------- /feature-flag/public/demo-marketing-offer1-exp-B.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/target-nodejs-sdk-samples/36c6921e6479202aad1ed437a104b8713fd939f7/feature-flag/public/demo-marketing-offer1-exp-B.png -------------------------------------------------------------------------------- /feature-flag/public/style.css: -------------------------------------------------------------------------------- 1 | #result { 2 | padding: 25px; 3 | background-color: lightcyan; 4 | } 5 | 6 | #result.error { 7 | background-color: lightpink; 8 | } 9 | 10 | .offer { 11 | width: 100%; 12 | max-width: 480px; 13 | } 14 | 15 | .offer img { 16 | width: 100%; 17 | height: auto; 18 | } 19 | 20 | input.searchbox { 21 | min-width: 480px; 22 | } 23 | -------------------------------------------------------------------------------- /feature-flag/sampleRules.json: -------------------------------------------------------------------------------- 1 | {"version":"1.0.0","meta":{"clientCode":"adobesummit2018","generatedAt":"2020-05-11T17:50:11.470Z","environment":"waters_test"},"globalMbox":"target-global-mbox","responseTokens":["activity.id","activity.name","experience.name","experience.id","offer.name","offer.id","option.id","option.name"],"remoteMboxes":["target-global-mbox"],"rules":{"mboxes":{"demo-marketing-offer2":[{"meta":{"activityId":345967,"activityType":"ab","experienceId":0,"locationName":"demo-marketing-offer2","locationType":"mbox","locationId":0,"audienceIds":[],"offerIds":[645951]},"condition":{"<":[0,{"var":"allocation"},50]},"consequence":{"name":"demo-marketing-offer2","options":[{"type":"html","content":"

Experience A

\n","eventToken":"HZX7npD8JbNQHNmhqCQpPmqipfsIHvVzTQxHolz2IpSCnQ9Y9OaLL2gsdrWQTvE5D1i1VIvi6hLUaKBzNXfVcQ=="}],"metrics":[]}},{"meta":{"activityId":345967,"activityType":"ab","experienceId":1,"locationName":"demo-marketing-offer2","locationType":"mbox","locationId":0,"audienceIds":[],"offerIds":[645950]},"condition":{"<":[50,{"var":"allocation"},100]},"consequence":{"name":"demo-marketing-offer2","options":[{"type":"html","content":"

Experience B

\n","eventToken":"HZX7npD8JbNQHNmhqCQpPpNWHtnQtQrJfmRrQugEa2qCnQ9Y9OaLL2gsdrWQTvE5D1i1VIvi6hLUaKBzNXfVcQ=="}],"metrics":[]}}],"demo-marketing-offer1":[{"meta":{"activityId":345822,"activityType":"ab","experienceId":0,"locationName":"demo-marketing-offer1","locationType":"mbox","locationId":0,"audienceIds":[],"offerIds":[645820]},"condition":{"<":[0,{"var":"allocation"},50]},"consequence":{"name":"demo-marketing-offer1","options":[{"type":"json","content":{"experience":"A","asset":"demo-marketing-offer1-exp-A.png"},"eventToken":"y+HbeDWrMK/iSRu7CyIQU2qipfsIHvVzTQxHolz2IpSCnQ9Y9OaLL2gsdrWQTvE5D1i1VIvi6hLUaKBzNXfVcQ=="}],"metrics":[]}},{"meta":{"activityId":345822,"activityType":"ab","experienceId":1,"locationName":"demo-marketing-offer1","locationType":"mbox","locationId":0,"audienceIds":[],"offerIds":[645821]},"condition":{"<":[50,{"var":"allocation"},100]},"consequence":{"name":"demo-marketing-offer1","options":[{"type":"json","content":{"experience":"B","asset":"demo-marketing-offer1-exp-B.png"},"eventToken":"y+HbeDWrMK/iSRu7CyIQU5NWHtnQtQrJfmRrQugEa2qCnQ9Y9OaLL2gsdrWQTvE5D1i1VIvi6hLUaKBzNXfVcQ=="}],"metrics":[]}}],"demo-engineering-flags":[{"meta":{"activityId":345998,"activityType":"ab","experienceId":0,"locationName":"demo-engineering-flags","locationType":"mbox","locationId":0,"audienceIds":[],"offerIds":[646021]},"condition":{"<":[0,{"var":"allocation"},50]},"consequence":{"name":"demo-engineering-flags","options":[{"type":"json","content":{"cdnHostname":"cdn.cloud.corp.net","searchProviderId":"starwars","hasLegacyAccess":false},"eventToken":"kmRGKfY0vzeilGwqmXicO2qipfsIHvVzTQxHolz2IpSCnQ9Y9OaLL2gsdrWQTvE5D1i1VIvi6hLUaKBzNXfVcQ=="}],"metrics":[]}},{"meta":{"activityId":345998,"activityType":"ab","experienceId":1,"locationName":"demo-engineering-flags","locationType":"mbox","locationId":0,"audienceIds":[],"offerIds":[646020]},"condition":{"<":[50,{"var":"allocation"},100]},"consequence":{"name":"demo-engineering-flags","options":[{"type":"json","content":{"cdnHostname":"cdn.megacloud.corp.com","searchProviderId":"startrek","hasLegacyAccess":true},"eventToken":"kmRGKfY0vzeilGwqmXicO5NWHtnQtQrJfmRrQugEa2qCnQ9Y9OaLL2gsdrWQTvE5D1i1VIvi6hLUaKBzNXfVcQ=="}],"metrics":[]}}],"target-global-mbox":[{"meta":{"activityId":345975,"activityType":"ab","experienceId":0,"locationName":"target-global-mbox","locationType":"view","locationId":0,"audienceIds":[5661671],"offerIds":[]},"condition":{"and":[{"<":[0,{"var":"allocation"},50]},{"and":[{"==":["127.0.0.1",{"var":"page.domain"}]},{"==":["/",{"var":"page.path"}]}]}]},"consequence":{"name":"target-global-mbox","options":[{"type":"actions","content":[{"type":"setImageSource","selector":"#offer > IMG.rounded:eq(0)","cssSelector":"#offer > IMG:nth-of-type(1)","content":"assets/demo-marketing-offer1-exp-A.png"}],"eventToken":"5mFU2TowClLcgXz1gLRoLGqipfsIHvVzTQxHolz2IpSCnQ9Y9OaLL2gsdrWQTvE5D1i1VIvi6hLUaKBzNXfVcQ=="}],"metrics":[]}},{"meta":{"activityId":345975,"activityType":"ab","experienceId":0,"locationName":"target-global-mbox","locationType":"view","locationId":1,"audienceIds":[5661671],"offerIds":[]},"condition":{"and":[{"<":[0,{"var":"allocation"},50]},{"and":[{"==":["127.0.0.1",{"var":"page.domain"}]},{"==":["/",{"var":"page.path"}]}]}]},"consequence":{"name":"target-global-mbox","options":[{"type":"actions","content":[{"type":"setHtml","selector":"#offer > P.lead:eq(0)","cssSelector":"#offer > P:nth-of-type(1)","content":"Experience A"}],"eventToken":"5mFU2TowClLcgXz1gLRoLGqipfsIHvVzTQxHolz2IpSCnQ9Y9OaLL2gsdrWQTvE5D1i1VIvi6hLUaKBzNXfVcQ=="}],"metrics":[]}},{"meta":{"activityId":345975,"activityType":"ab","experienceId":1,"locationName":"target-global-mbox","locationType":"view","locationId":0,"audienceIds":[5661671],"offerIds":[]},"condition":{"and":[{"<":[50,{"var":"allocation"},100]},{"and":[{"==":["127.0.0.1",{"var":"page.domain"}]},{"==":["/",{"var":"page.path"}]}]}]},"consequence":{"name":"target-global-mbox","options":[{"type":"actions","content":[{"type":"setImageSource","selector":"#offer > IMG.rounded:eq(0)","cssSelector":"#offer > IMG:nth-of-type(1)","content":"assets/demo-marketing-offer1-exp-B.png"}],"eventToken":"5mFU2TowClLcgXz1gLRoLJNWHtnQtQrJfmRrQugEa2qCnQ9Y9OaLL2gsdrWQTvE5D1i1VIvi6hLUaKBzNXfVcQ=="}],"metrics":[]}},{"meta":{"activityId":345975,"activityType":"ab","experienceId":1,"locationName":"target-global-mbox","locationType":"view","locationId":1,"audienceIds":[5661671],"offerIds":[]},"condition":{"and":[{"<":[50,{"var":"allocation"},100]},{"and":[{"==":["127.0.0.1",{"var":"page.domain"}]},{"==":["/",{"var":"page.path"}]}]}]},"consequence":{"name":"target-global-mbox","options":[{"type":"actions","content":[{"type":"setHtml","selector":"#offer > P.lead:eq(0)","cssSelector":"#offer > P:nth-of-type(1)","content":"Experience B"}],"eventToken":"5mFU2TowClLcgXz1gLRoLJNWHtnQtQrJfmRrQugEa2qCnQ9Y9OaLL2gsdrWQTvE5D1i1VIvi6hLUaKBzNXfVcQ=="}],"metrics":[]}}]},"views":{}}} 2 | -------------------------------------------------------------------------------- /feature-flag/server.js: -------------------------------------------------------------------------------- 1 | /*************************************************************************************** 2 | * (c) 2019 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | ****************************************************************************************/ 12 | 13 | const _ = require("lodash"); 14 | const fs = require("fs"); 15 | const express = require("express"); 16 | const cookieParser = require("cookie-parser"); 17 | const Handlebars = require("handlebars"); 18 | const TargetClient = require("@adobe/target-nodejs-sdk"); 19 | const { getSearchProvider } = require("./provider"); 20 | 21 | const PAGE_TITLE = "Target On-Device Decisioning Sample"; 22 | // Load the template of the HTML page returned in the response 23 | const TEMPLATE = fs.readFileSync(`${__dirname}/index.handlebars`).toString(); 24 | const handlebarsTemplate = Handlebars.compile(TEMPLATE); 25 | 26 | const app = express(); 27 | app.use(express.static("public")); 28 | app.use(cookieParser()); 29 | 30 | const RESPONSE_HEADERS = { 31 | "Content-Type": "text/html", 32 | Expires: new Date().toUTCString(), 33 | }; 34 | 35 | function saveCookie(res, cookie) { 36 | if (!cookie) { 37 | return; 38 | } 39 | 40 | res.cookie(cookie.name, cookie.value, { maxAge: cookie.maxAge * 1000 }); 41 | } 42 | 43 | function sendHtml(res, pageContext) { 44 | const html = handlebarsTemplate({ 45 | ...pageContext, 46 | pageTitle: PAGE_TITLE, 47 | }); 48 | 49 | res.status(200).send(html); 50 | } 51 | 52 | function sendSuccessResponse(res, targetResponse, pageContext = {}) { 53 | res.set(RESPONSE_HEADERS); 54 | saveCookie(res, targetResponse.targetCookie); 55 | 56 | sendHtml(res, { 57 | ...pageContext, 58 | targetResponse: JSON.stringify(targetResponse, null, 4), 59 | }); 60 | } 61 | 62 | function sendErrorResponse(res, error) { 63 | res.set(RESPONSE_HEADERS); 64 | sendHtml(res, { 65 | error: true, 66 | targetResponse: `ERROR: ${error.message}`, 67 | }); 68 | } 69 | 70 | const CONFIG = { 71 | client: "adobesummit2018", 72 | organizationId: "65453EA95A70434F0A495D34@AdobeOrg", 73 | decisioningMethod: "on-device", 74 | artifactPayload: require("./sampleRules"), 75 | events: { 76 | clientReady: startExpressApp 77 | }, 78 | }; 79 | 80 | const targetClient = TargetClient.create(CONFIG); 81 | 82 | async function startExpressApp() { 83 | app.get("/", async (req, res) => { 84 | const targetCookie = req.cookies[TargetClient.TargetCookieName]; 85 | try { 86 | const offerAttributes = await targetClient.getAttributes( 87 | ["demo-engineering-flags", "demo-marketing-offer1"], 88 | { targetCookie } 89 | ); 90 | 91 | const searchProviderId = offerAttributes.getValue( 92 | "demo-engineering-flags", 93 | "searchProviderId" 94 | ); 95 | 96 | const searchProvider = getSearchProvider(searchProviderId); 97 | 98 | let searchResult; 99 | 100 | if (req.query.search) { 101 | searchResult = await searchProvider.execute(req.query.search); 102 | } 103 | 104 | sendSuccessResponse(res, offerAttributes.getResponse(), { 105 | search: { 106 | domain: searchProvider.domain, 107 | result: searchResult, 108 | }, 109 | flags: JSON.stringify(offerAttributes.asObject(), null, 4), 110 | offer: offerAttributes.asObject("demo-marketing-offer1"), 111 | }); 112 | } catch (error) { 113 | console.error("Target:", error); 114 | sendErrorResponse(res, error); 115 | } 116 | }); 117 | 118 | app.listen(3000, function () { 119 | console.log("Listening on port 3000 and watching!"); 120 | }); 121 | } 122 | -------------------------------------------------------------------------------- /multiple-mbox-ecid-analytics-atjs-integration/README.md: -------------------------------------------------------------------------------- 1 | # Multiple mbox ECID, Analytics and at.js integration sample 2 | 3 | This sample is similar to "ECID, Analytics and at.js integration" sample, additionaly exemplifying the inclusion of 4 | multiple mboxes in the Target Delivery API request. 5 | 6 | ## Usage 7 | 1. Install dependencies: `npm i` 8 | 2. Start: `npm start` 9 | -------------------------------------------------------------------------------- /multiple-mbox-ecid-analytics-atjs-integration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multiple-mbox-ecid-analytics-atjs-integration", 3 | "version": "1.0.0", 4 | "description": "Adobe Target Node.js SDK, multiple-mbox-ecid-analytics-atjs-integration sample", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:adobe/target-nodejs-sdk-samples.git" 12 | }, 13 | "keywords": [ 14 | "NodeJS", 15 | "Server", 16 | "API", 17 | "Adobe", 18 | "Target", 19 | "MCID", 20 | "ECID", 21 | "Visitor", 22 | "Delivery", 23 | "multiple-mbox-ecid-analytics-atjs-integration" 24 | ], 25 | "author": "Adobe Systems Inc.", 26 | "license": "Apache-2.0", 27 | "dependencies": { 28 | "@adobe/target-nodejs-sdk": "^2.1.0", 29 | "cookie-parser": "^1.4.4", 30 | "express": "^4.17.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /multiple-mbox-ecid-analytics-atjs-integration/server.js: -------------------------------------------------------------------------------- 1 | /*************************************************************************************** 2 | * (c) 2019 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | ****************************************************************************************/ 12 | 13 | const fs = require("fs"); 14 | const express = require("express"); 15 | const cookieParser = require("cookie-parser"); 16 | const TargetClient = require("@adobe/target-nodejs-sdk"); 17 | const CONFIG = { 18 | client: "adobetargetmobile", 19 | organizationId: "B8A054D958807F770A495DD6@AdobeOrg", 20 | timeout: 10000, 21 | logger: console 22 | }; 23 | const targetClient = TargetClient.create(CONFIG); 24 | const TEMPLATE = fs.readFileSync(__dirname + "/templates/index.tpl").toString(); 25 | 26 | const app = express(); 27 | app.use(cookieParser()); 28 | app.use(express.static(__dirname + "/public")); 29 | 30 | function saveCookie(res, cookie) { 31 | if (!cookie) { 32 | return; 33 | } 34 | 35 | res.cookie(cookie.name, cookie.value, { maxAge: cookie.maxAge * 1000 }); 36 | } 37 | 38 | const getResponseHeaders = () => ({ 39 | "Content-Type": "text/html", 40 | Expires: new Date().toUTCString() 41 | }); 42 | 43 | function sendHtml(res, offer) { 44 | const htmlResponse = TEMPLATE.replace( 45 | "${organizationId}", 46 | CONFIG.organizationId 47 | ) 48 | .replace("${visitorState}", JSON.stringify(offer.visitorState)) 49 | .replace("${content}", JSON.stringify(offer, null, " ")); 50 | 51 | res.status(200).send(htmlResponse); 52 | } 53 | 54 | function sendSuccessResponse(res, response) { 55 | res.set(getResponseHeaders()); 56 | saveCookie(res, response.targetCookie); 57 | saveCookie(res, response.targetLocationHintCookie); 58 | sendHtml(res, response); 59 | } 60 | 61 | function sendErrorResponse(res, error) { 62 | res.set(getResponseHeaders()); 63 | res.status(500).send(error); 64 | } 65 | 66 | function getAddress(req) { 67 | return { url: req.headers.host + req.originalUrl }; 68 | } 69 | 70 | app.get("/", async (req, res) => { 71 | const visitorCookie = 72 | req.cookies[ 73 | encodeURIComponent( 74 | TargetClient.getVisitorCookieName(CONFIG.organizationId) 75 | ) 76 | ]; 77 | const targetCookie = req.cookies[TargetClient.TargetCookieName]; 78 | const targetLocationHintCookie = 79 | req.cookies[TargetClient.TargetLocationHintCookieName]; 80 | const request = { 81 | execute: { 82 | mboxes: [ 83 | { 84 | address: getAddress(req), 85 | name: "a1-serverside-ab", 86 | profileParameters: { 87 | country: "usa" 88 | } 89 | }, 90 | { 91 | address: getAddress(req), 92 | name: "a1-serverside-xt", 93 | profileParameters: { 94 | country: "usa" 95 | } 96 | } 97 | ] 98 | } 99 | }; 100 | 101 | try { 102 | const response = await targetClient.getOffers({ 103 | request, 104 | visitorCookie, 105 | targetCookie, 106 | targetLocationHintCookie 107 | }); 108 | sendSuccessResponse(res, response); 109 | } catch (error) { 110 | console.error("Target:", error); 111 | sendErrorResponse(res, error); 112 | } 113 | }); 114 | 115 | app.listen(3000, function() { 116 | console.log("Listening on port 3000 and watching!"); 117 | }); 118 | -------------------------------------------------------------------------------- /multiple-mbox-ecid-analytics-atjs-integration/templates/index.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Multiple Mbox ECID (Visitor API) with Analytics and at.js Integration Sample 6 | 7 | 10 | 15 | 16 | 17 | 18 |
${content}
19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /next-server-side-rendering-demo/.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | node_modules 3 | .DS_Store 4 | *.iml 5 | node_modules 6 | npm-debug.log 7 | build 8 | coverage 9 | checkstyle 10 | dist 11 | .idea 12 | -------------------------------------------------------------------------------- /next-server-side-rendering-demo/README.md: -------------------------------------------------------------------------------- 1 | # Target server-side rendering with Next.js sample 2 | 3 | A simple TargetClient SDK usage in server-side rendering demo, based on the Next.js sample app. 4 | Target offers are prefetched on the server-side and included in the rendered page, thus enabling instant application 5 | of Target offers on the client side, without any additional Target calls initiated by Target client at.js library. 6 | 7 | ## Usage 8 | 1. Install dependencies: `npm i` 9 | 2. Dev build: `npm run dev` 10 | 3. Prod build: `npm run build && npm start` 11 | -------------------------------------------------------------------------------- /next-server-side-rendering-demo/components/Header.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | const linkStyle = { 4 | marginRight: 15 5 | } 6 | 7 | export default function Header() { 8 | return ( 9 |
10 | 11 | Home 12 | 13 | 14 | About 15 | 16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /next-server-side-rendering-demo/components/MyLayout.js: -------------------------------------------------------------------------------- 1 | import Header from './Header' 2 | 3 | const layoutStyle = { 4 | margin: 20, 5 | padding: 20, 6 | border: '1px solid #DDD' 7 | } 8 | 9 | export default function Layout(props) { 10 | return ( 11 |
12 |
13 | {props.children} 14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /next-server-side-rendering-demo/helpers/target-client-side.js: -------------------------------------------------------------------------------- 1 | function triggerView(viewName) { 2 | if (typeof adobe != 'undefined' && adobe.target && typeof adobe.target.triggerView === 'function') { 3 | adobe.target.triggerView(viewName); 4 | } 5 | } 6 | 7 | export default { 8 | triggerView 9 | }; 10 | -------------------------------------------------------------------------------- /next-server-side-rendering-demo/helpers/target-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "client": "adobetargetmobile", 3 | "organizationId": "B8A054D958807F770A495DD6@AdobeOrg", 4 | "timeout": 10000, 5 | "serverDomain": "adobetargetmobile.tt.omtrdc.net" 6 | } 7 | -------------------------------------------------------------------------------- /next-server-side-rendering-demo/helpers/target-server-side.js: -------------------------------------------------------------------------------- 1 | import { parseCookies, setCookie } from "nookies"; 2 | import TargetClient from "@adobe/target-nodejs-sdk"; 3 | import CONFIG from "./target-config.json"; 4 | 5 | const targetOptions = Object.assign({ logger: console }, CONFIG); 6 | const targetClient = TargetClient.create(targetOptions); 7 | 8 | function saveCookie(ctx, cookie) { 9 | if (!cookie) { 10 | return; 11 | } 12 | 13 | setCookie(ctx, cookie.name, cookie.value, { maxAge: cookie.maxAge * 1000 }); 14 | } 15 | 16 | function getTargetOptions(ctx) { 17 | const cookies = parseCookies(ctx); 18 | 19 | return { 20 | targetCookie: 21 | cookies[encodeURIComponent(TargetClient.TargetCookieName)], 22 | targetLocationHintCookie: 23 | cookies[ 24 | encodeURIComponent(TargetClient.TargetLocationHintCookieName) 25 | ] 26 | }; 27 | } 28 | 29 | function setTraceToken(request = {}, ctx) { 30 | const { trace={} } = request; 31 | const { authorizationToken = ctx.query.authorization } = trace; 32 | 33 | if (!authorizationToken || typeof authorizationToken !== "string") { 34 | return request; 35 | } 36 | 37 | return { 38 | ...request, 39 | trace: { 40 | ...trace, 41 | authorizationToken 42 | } 43 | } 44 | } 45 | 46 | function setResponseCookies(ctx, response) { 47 | saveCookie(ctx, response.targetCookie); 48 | saveCookie(ctx, response.targetLocationHintCookie); 49 | } 50 | 51 | async function prefetchOffers(ctx) { 52 | const { req } = ctx; 53 | if (!req) { 54 | return {}; 55 | } 56 | const requestURL = ctx.req.headers.host + ctx.asPath; 57 | let prefetchViewsRequest = { 58 | prefetch: { 59 | views: [{ address: { url: requestURL } }] 60 | } 61 | }; 62 | 63 | prefetchViewsRequest = setTraceToken(prefetchViewsRequest, ctx); 64 | 65 | const options = Object.assign( 66 | { request: prefetchViewsRequest }, 67 | getTargetOptions(ctx) 68 | ); 69 | 70 | const { organizationId, client, serverDomain = "" } = CONFIG; 71 | const result = { 72 | organizationId, 73 | client, 74 | serverDomain 75 | }; 76 | 77 | try { 78 | const response = await targetClient.getOffers(options); 79 | setResponseCookies(ctx, response); 80 | result.visitorState = JSON.stringify(response.visitorState); 81 | result.serverState = { 82 | request: response.request, 83 | response: response.response 84 | }; 85 | } catch (e) { 86 | console.error("Target: Error prefetching offers: ", e); 87 | } 88 | 89 | return result; 90 | } 91 | 92 | function visitorInit(props) { 93 | return `Visitor.getInstance("${ 94 | props.target.organizationId 95 | }", {serverState: ${JSON.stringify(props.target.visitorState || {})}});`; 96 | } 97 | 98 | function targetInit(props) { 99 | return `window.targetGlobalSettings = { 100 | overrideMboxEdgeServer: true, 101 | clientCode: "${props.target.client}", 102 | imsOrgId: "${props.target.organizationId}", 103 | serverDomain: "${props.target.serverDomain}", 104 | serverState: ${JSON.stringify(props.target.serverState || {}, null, " ")} 105 | };`; 106 | } 107 | 108 | function analyticsBeacon() { 109 | return "var s_code=s.t();if(s_code)document.write(s_code);"; 110 | } 111 | 112 | export default { 113 | prefetchOffers, 114 | visitorInit, 115 | targetInit, 116 | analyticsBeacon 117 | }; 118 | -------------------------------------------------------------------------------- /next-server-side-rendering-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "target-nodejs-ssr-sample", 3 | "version": "1.0.0", 4 | "description": "Adobe Target NodeJS SDK SSR sample", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "next", 8 | "build": "next build", 9 | "start": "next start -p 4445", 10 | "start-prod": "next build && next start -p $PORT" 11 | }, 12 | "keywords": [], 13 | "repository": { 14 | "type": "git", 15 | "url": "git@github.com:adobe/target-nodejs-sdk-samples.git" 16 | }, 17 | "author": "Adobe Systems Inc.", 18 | "license": "Apache-2.0", 19 | "browser": { 20 | "@adobe/target-nodejs-sdk": false 21 | }, 22 | "dependencies": { 23 | "@adobe/target-nodejs-sdk": "^2.1.0", 24 | "isomorphic-unfetch": "^3.0.0", 25 | "next": "^9.0.0", 26 | "nookies": "^2.0.8", 27 | "react": "^16.8.4", 28 | "react-dom": "^16.8.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /next-server-side-rendering-demo/pages/_document.js: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from 'next/document'; 2 | import Target from '../helpers/target-server-side'; 3 | 4 | class MyDocument extends Document { 5 | static async getInitialProps(ctx) { 6 | const initialProps = await Document.getInitialProps(ctx); 7 | const targetProps = await Target.prefetchOffers(ctx); 8 | return { ...initialProps, target: targetProps }; 9 | } 10 | 11 | render() { 12 | return ( 13 | 14 | 15 | 25 | 26 | 27 |
28 | 29 | 30 | 16 | 19 | 28 | 29 | 30 | 31 |
32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "target-nodejs-react-sample", 3 | "version": "1.0.0", 4 | "description": "Adobe Target NodeJS SDK React sample", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "webpack --mode=development", 8 | "build": "webpack --mode=production", 9 | "start": "PORT=4444 node server.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git@github.com:adobe/target-nodejs-sdk-samples.git" 14 | }, 15 | "author": "Adobe Systems Inc.", 16 | "license": "Apache-2.0", 17 | "dependencies": { 18 | "@adobe/target-nodejs-sdk": "^2.1.0", 19 | "axios": "^0.19.0", 20 | "cookie-parser": "^1.4.4", 21 | "express": "^4.16.4", 22 | "react": "^16.8.1", 23 | "react-dom": "^16.8.1", 24 | "react-redux": "^6.0.0", 25 | "react-router": "^4.3.1", 26 | "react-router-dom": "^4.3.1", 27 | "react-router-redux": "^4.0.8", 28 | "react-slick": "^0.23.2", 29 | "redux": "^3.7.2", 30 | "redux-thunk": "^2.3.0" 31 | }, 32 | "devDependencies": { 33 | "babel-cli": "^6.26.0", 34 | "babel-core": "^6.26.3", 35 | "babel-loader": "^7.1.5", 36 | "babel-preset-es2015": "^6.18.0", 37 | "babel-preset-env": "^1.7.0", 38 | "babel-preset-react": "^6.24.1", 39 | "copy-webpack-plugin": "^4.6.0", 40 | "file-loader": "^3.0.1", 41 | "randomstring": "^1.1.5", 42 | "react-helmet": "^5.2.0", 43 | "uglifyjs-webpack-plugin": "^2.1.1", 44 | "url-loader": "^1.1.2", 45 | "webpack": "^4.29.5", 46 | "webpack-cli": "^3.2.3" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/public/assets/css/app.css: -------------------------------------------------------------------------------- 1 | .loading-wrapper { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100vw; 6 | height: 100vh; 7 | z-index: 5; 8 | background: #fff; } 9 | .loading-wrapper .loading { 10 | display: table; 11 | width: 100vw; 12 | height: 100vh; } 13 | .loading-wrapper .loading span { 14 | display: table-cell; 15 | vertical-align: middle; 16 | text-align: center; 17 | width: 100vw; 18 | height: 100vh; } 19 | .loading-wrapper.is-loading { 20 | visibility: visible; } 21 | .loading-wrapper.is-loaded { 22 | -webkit-animation: hideLoading 1s forwards; 23 | -moz-animation: hideLoading 1s forwards; 24 | -o-animation: hideLoading 1s forwards; 25 | animation: hideLoading 1s forwards; } 26 | 27 | .btn-margin { 28 | margin-top: 1.5rem; 29 | margin-right: 1.5rem; } 30 | 31 | .badge { 32 | font-size: 1.5rem; 33 | margin-top: 1rem; 34 | margin-bottom: 1rem; } 35 | 36 | .slick-next:before, .slick-prev:before { 37 | color: #e41f1f; } 38 | 39 | .notice { 40 | border-radius: 5px; 41 | -moz-border-radius: 5px; 42 | -webkit-border-radius: 5px; 43 | border: 2px solid #a41c23; 44 | padding: 0.3rem; 45 | margin-top: 0.5rem; 46 | margin-bottom: 0.3rem; 47 | background-color: #c5eff5; 48 | text-align: center; 49 | color: #4682b4; } 50 | 51 | .button.menu { 52 | background-color: #3273dc; 53 | border-color: #3273dc; 54 | color: #ffffff; } 55 | 56 | .button.menu:focus, .button.menu:hover { 57 | background-color: #cd1e27; 58 | border-color: #f1560c; 59 | color: #ffffff; } 60 | 61 | .section { 62 | padding: 1.5rem; } 63 | 64 | .nav-item img { 65 | max-height: 4rem; } 66 | 67 | .heading { 68 | text-transform: none; } 69 | 70 | body.isLoading { 71 | overflow: hidden; } 72 | 73 | @-webkit-keyframes hideLoading { 74 | 0% { 75 | visibility: visible; } 76 | 100% { 77 | visibility: hidden; } } 78 | @-moz-keyframes hideLoading { 79 | 0% { 80 | visibility: visible; } 81 | 100% { 82 | visibility: hidden; } } 83 | @-o-keyframes hideLoading { 84 | 0% { 85 | visibility: visible; } 86 | 100% { 87 | visibility: hidden; } } 88 | @keyframes hideLoading { 89 | 0% { 90 | visibility: visible; } 91 | 100% { 92 | visibility: hidden; } } 93 | 94 | 95 | /* CUSTOM STYLES */ 96 | :root { 97 | overflow: auto; 98 | } 99 | 100 | .footer { 101 | margin-top: 30px; 102 | } 103 | 104 | :root, body, #app, .app-container { 105 | height: 100%; 106 | } 107 | 108 | .page-container { 109 | min-height: calc(100% - 156px); 110 | } 111 | 112 | .Footernav-disclaimers { 113 | float: right; 114 | font-size: 12px; 115 | } 116 | 117 | li.Footernav-disclaimer { 118 | display: inline-block; 119 | } 120 | 121 | li:not(:last-child).Footernav-disclaimer:after { 122 | content: '/'; 123 | margin: 0 8px; 124 | } 125 | 126 | .sub-text { 127 | font-size: 11px; 128 | } 129 | 130 | .slick-slide img { 131 | width: 100% !important; 132 | } 133 | 134 | .cart-popover { 135 | right: 25px !important; 136 | width: 400px !important; 137 | left: auto !important; 138 | top: 56px !important; 139 | max-width: 100% !important; 140 | } 141 | 142 | /*# sourceMappingURL=app.css.map */ 143 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/public/assets/css/app.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "mappings": "AAAA,gBAAgB;EACZ,QAAQ,EAAC,KAAK;EACd,GAAG,EAAC,CAAC;EACL,IAAI,EAAC,CAAC;EACN,KAAK,EAAE,KAAK;EACZ,MAAM,EAAE,KAAK;EACb,OAAO,EAAE,CAAC;EACV,UAAU,EAAE,IAAI;EAChB,yBAAQ;IACJ,OAAO,EAAE,KAAK;IACd,KAAK,EAAE,KAAK;IACZ,MAAM,EAAE,KAAK;IACb,8BAAI;MACA,OAAO,EAAE,UAAU;MACnB,cAAc,EAAE,MAAM;MACtB,UAAU,EAAE,MAAM;MAClB,KAAK,EAAE,KAAK;MACZ,MAAM,EAAE,KAAK;EAGrB,2BAAY;IACR,UAAU,EAAG,OAAO;EAExB,0BAAW;IACP,iBAAiB,EAAE,uBAAuB;IAC1C,cAAc,EAAE,uBAAuB;IACvC,YAAY,EAAE,uBAAuB;IACrC,SAAS,EAAE,uBAAuB;;AAI1C,QAAS;EACP,UAAU,EAAE,KAAK;;AAGnB,WAAY;EACV,UAAU,EAAE,MAAM;EAClB,YAAY,EAAE,MAAM;;AAGtB,MAAO;EACL,SAAS,EAAE,MAAM;EACjB,UAAU,EAAE,IAAI;EAChB,aAAa,EAAE,IAAI;;AAErB,sCAAuC;EACrC,KAAK,EAAE,OAAO;;AAGhB,OAAQ;EACN,aAAa,EAAE,GAAG;EAClB,kBAAkB,EAAE,GAAG;EACvB,qBAAqB,EAAE,GAAG;EAC1B,MAAM,EAAE,iBAAiB;EACzB,OAAO,EAAE,MAAM;EACf,UAAU,EAAE,MAAM;EAClB,aAAa,EAAE,MAAM;EACrB,gBAAgB,EAAE,OAAO;EACzB,UAAU,EAAE,MAAM;EAClB,KAAK,EAAE,OAAO;;AAGhB,YAAa;EACX,gBAAgB,EAAE,OAAO;EACzB,YAAY,EAAE,OAAO;EACrB,KAAK,EAAE,OAAO;;AAIhB,sCAAuC;EACrC,gBAAgB,EAAE,OAAO;EACzB,YAAY,EAAE,OAAO;EACrB,KAAK,EAAE,OAAO;;AAGhB,QAAS;EACP,OAAO,EAAE,WAAW;;AAGlB,aAAI;EACF,UAAU,EAAG,IAAI;;AAIvB,QAAS;EACP,cAAc,EAAE,IAAI;;AAIlB,cAAW;EACP,QAAQ,EAAE,MAAM;;AAIxB,8BAGC;EAFC,EAAK;IAAE,UAAU,EAAG,OAAO;EAC3B,IAAK;IAAE,UAAU,EAAG,MAAM;AAE5B,2BAGC;EAFC,EAAK;IAAE,UAAU,EAAG,OAAO;EAC3B,IAAK;IAAE,UAAU,EAAG,MAAM;AAE5B,yBAGC;EAFC,EAAK;IAAE,UAAU,EAAG,OAAO;EAC3B,IAAK;IAAE,UAAU,EAAG,MAAM;AAE5B,sBAGC;EAFA,EAAK;IAAE,UAAU,EAAG,OAAO;EAC1B,IAAK;IAAE,UAAU,EAAG,MAAM", 4 | "sources": ["../../../src/resources/sass/app.scss"], 5 | "names": [], 6 | "file": "app.css" 7 | } -------------------------------------------------------------------------------- /react-shopping-cart-demo/public/assets/resources/data/latestProducts.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "This luxurious sweater will look great with everything from jeans to chinos, Classic fit, Cotton/polyester, Machine washable, Imported", 4 | "id": 1, 5 | "image": "/assets/resources/images/poc.png", 6 | "price": 29.0, 7 | "title": "Danbury grey sweater with round buttons", 8 | "userId": 1 9 | }, 10 | { 11 | "description": "Adjustable, foldable headband, Controls: Volume, play/pause, answers/end call, 2 years Warranty", 12 | "id": 5, 13 | "image": "/assets/resources/images/products/headphone.png", 14 | "price": 45.0, 15 | "title": "DBeatz wireless headphones", 16 | "userId": 1 17 | }, 18 | { 19 | "description": "Built for lightweight performance, Men's athletic footwear from Airz, Synthetic upper; rubber sole.", 20 | "id": 9, 21 | "image": "/assets/resources/images/products/shoes.png", 22 | "price": 59.0, 23 | "title": "Airz Running Sneakers for men", 24 | "userId": 1 25 | }, 26 | { 27 | "description": "Dress Watch from Euro Style, Round case, 44mm, Water resistance, Warranty: 3-year limited", 28 | "id": 12, 29 | "image": "/assets/resources/images/products/watch.png", 30 | "price": 75, 31 | "title": "Men’s Black Stainless Steel Watch", 32 | "userId": 2 33 | } 34 | ] 35 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/public/assets/resources/data/products.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "This luxurious sweater will look great with everything from jeans to chinos, Classic fit, Cotton/polyester, Machine washable, Imported", 4 | "id": 1, 5 | "image": "/assets/resources/images/poc.png", 6 | "price": 29.0, 7 | "title": "Danbury grey sweater with round buttons", 8 | "userId": 1 9 | }, 10 | { 11 | "description": "Fashionable and functional medium size tote with a triple-compartment interior and plenty of pockets, Magnetic-snap closure, Gold-tone exterior hardware & logo charm, Imported", 12 | "id": 2, 13 | "image": "/assets/resources/images/products/bag.jpeg", 14 | "price": 59.0, 15 | "title": "Cara black tote handbag ", 16 | "userId": 1 17 | }, 18 | { 19 | "description": "Delicate flowers decorate this classic jean dress by Amore, Jean Pants – Pull on style, Machine Washable.", 20 | "id": 3, 21 | "image": "/assets/resources/images/products/baby.png", 22 | "price": 30, 23 | "title": "Amore floral print dress and pants set", 24 | "userId": 1 25 | }, 26 | { 27 | "description": "Spring inspired floral look, All cotton, Machine washable", 28 | "id": 4, 29 | "image": "/assets/resources/images/products/floraldress.png", 30 | "price": 29.0, 31 | "title": "Short floral spring dress", 32 | "userId": 1 33 | }, 34 | { 35 | "description": "Adjustable, foldable headband, Controls: Volume, play/pause, answers/end call, 2 years Warranty", 36 | "id": 5, 37 | "image": "/assets/resources/images/products/headphone.png", 38 | "price": 45.0, 39 | "title": "DBeatz wireless headphones", 40 | "userId": 1 41 | }, 42 | { 43 | "description": "Hardcase matte-finish plastic, striped polyester lining, 1 yr Warranty.", 44 | "id": 6, 45 | "image": "/assets/resources/images/products/luggage.png", 46 | "price": 119.0, 47 | "title": "Voyage 3- piece luggage set", 48 | "userId": 1 49 | }, 50 | { 51 | "description": "Rich botanicals helps hydrate sensitive skin and reduce appearance of fine lines and wrinkles.", 52 | "id": 7, 53 | "image": "/assets/resources/images/products/moisturizer.png", 54 | "price": 99.0, 55 | "title": "Roselle age defying moisturizer", 56 | "userId": 1 57 | }, 58 | { 59 | "description": "Fresh Floral Fragance with hints of Rose and Jasmine, Eau de Parfum Spray, 3.4 oz.", 60 | "id": 8, 61 | "image": "/assets/resources/images/products/perfume.png", 62 | "price": 86, 63 | "title": "MADEMOISELLE collection for women", 64 | "userId": 1 65 | }, 66 | { 67 | "description": "Built for lightweight performance, Men's athletic footwear from Airz, Synthetic upper; rubber sole.", 68 | "id": 9, 69 | "image": "/assets/resources/images/products/shoes.png", 70 | "price": 59.0, 71 | "title": "Airz Running Sneakers for men", 72 | "userId": 1 73 | }, 74 | { 75 | "description": "Ultra-soft and extra-absorbent, Imported, Mashine Washable", 76 | "id": 10, 77 | "image": "/assets/resources/images/products/towels.png", 78 | "price": 19.0, 79 | "title": "Multi color Luxe Cotton Towels", 80 | "userId": 1 81 | }, 82 | { 83 | "description": "Modern comfort long-sleeved cotton t-shirt, Machine Washable", 84 | "id": 11, 85 | "image": "/assets/resources/images/products/tshirt.png", 86 | "price": 18.0, 87 | "title": "White AlfaMen Casual Tshirt", 88 | "userId": 2 89 | }, 90 | { 91 | "description": "Dress Watch from Euro Style, Round case, 44mm, Water resistance, Warranty: 3-year limited", 92 | "id": 12, 93 | "image": "/assets/resources/images/products/watch.png", 94 | "price": 75, 95 | "title": "Men’s Black Stainless Steel Watch", 96 | "userId": 2 97 | } 98 | ] 99 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/public/assets/resources/images/carousel/bigsale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/target-nodejs-sdk-samples/36c6921e6479202aad1ed437a104b8713fd939f7/react-shopping-cart-demo/public/assets/resources/images/carousel/bigsale.png -------------------------------------------------------------------------------- /react-shopping-cart-demo/public/assets/resources/images/carousel/discount.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/target-nodejs-sdk-samples/36c6921e6479202aad1ed437a104b8713fd939f7/react-shopping-cart-demo/public/assets/resources/images/carousel/discount.png -------------------------------------------------------------------------------- /react-shopping-cart-demo/public/assets/resources/images/carousel/easter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/target-nodejs-sdk-samples/36c6921e6479202aad1ed437a104b8713fd939f7/react-shopping-cart-demo/public/assets/resources/images/carousel/easter.png -------------------------------------------------------------------------------- /react-shopping-cart-demo/public/assets/resources/images/carousel/family.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/target-nodejs-sdk-samples/36c6921e6479202aad1ed437a104b8713fd939f7/react-shopping-cart-demo/public/assets/resources/images/carousel/family.png -------------------------------------------------------------------------------- /react-shopping-cart-demo/public/assets/resources/images/carousel/happy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/target-nodejs-sdk-samples/36c6921e6479202aad1ed437a104b8713fd939f7/react-shopping-cart-demo/public/assets/resources/images/carousel/happy.png -------------------------------------------------------------------------------- /react-shopping-cart-demo/public/assets/resources/images/carousel/percent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/target-nodejs-sdk-samples/36c6921e6479202aad1ed437a104b8713fd939f7/react-shopping-cart-demo/public/assets/resources/images/carousel/percent.png -------------------------------------------------------------------------------- /react-shopping-cart-demo/public/assets/resources/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/target-nodejs-sdk-samples/36c6921e6479202aad1ed437a104b8713fd939f7/react-shopping-cart-demo/public/assets/resources/images/logo.png -------------------------------------------------------------------------------- /react-shopping-cart-demo/public/assets/resources/images/poc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/target-nodejs-sdk-samples/36c6921e6479202aad1ed437a104b8713fd939f7/react-shopping-cart-demo/public/assets/resources/images/poc.png -------------------------------------------------------------------------------- /react-shopping-cart-demo/public/assets/resources/images/products/baby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/target-nodejs-sdk-samples/36c6921e6479202aad1ed437a104b8713fd939f7/react-shopping-cart-demo/public/assets/resources/images/products/baby.png -------------------------------------------------------------------------------- /react-shopping-cart-demo/public/assets/resources/images/products/bag.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/target-nodejs-sdk-samples/36c6921e6479202aad1ed437a104b8713fd939f7/react-shopping-cart-demo/public/assets/resources/images/products/bag.jpeg -------------------------------------------------------------------------------- /react-shopping-cart-demo/public/assets/resources/images/products/floraldress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/target-nodejs-sdk-samples/36c6921e6479202aad1ed437a104b8713fd939f7/react-shopping-cart-demo/public/assets/resources/images/products/floraldress.png -------------------------------------------------------------------------------- /react-shopping-cart-demo/public/assets/resources/images/products/headphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/target-nodejs-sdk-samples/36c6921e6479202aad1ed437a104b8713fd939f7/react-shopping-cart-demo/public/assets/resources/images/products/headphone.png -------------------------------------------------------------------------------- /react-shopping-cart-demo/public/assets/resources/images/products/luggage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/target-nodejs-sdk-samples/36c6921e6479202aad1ed437a104b8713fd939f7/react-shopping-cart-demo/public/assets/resources/images/products/luggage.png -------------------------------------------------------------------------------- /react-shopping-cart-demo/public/assets/resources/images/products/moisturizer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/target-nodejs-sdk-samples/36c6921e6479202aad1ed437a104b8713fd939f7/react-shopping-cart-demo/public/assets/resources/images/products/moisturizer.png -------------------------------------------------------------------------------- /react-shopping-cart-demo/public/assets/resources/images/products/perfume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/target-nodejs-sdk-samples/36c6921e6479202aad1ed437a104b8713fd939f7/react-shopping-cart-demo/public/assets/resources/images/products/perfume.png -------------------------------------------------------------------------------- /react-shopping-cart-demo/public/assets/resources/images/products/shoes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/target-nodejs-sdk-samples/36c6921e6479202aad1ed437a104b8713fd939f7/react-shopping-cart-demo/public/assets/resources/images/products/shoes.png -------------------------------------------------------------------------------- /react-shopping-cart-demo/public/assets/resources/images/products/towels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/target-nodejs-sdk-samples/36c6921e6479202aad1ed437a104b8713fd939f7/react-shopping-cart-demo/public/assets/resources/images/products/towels.png -------------------------------------------------------------------------------- /react-shopping-cart-demo/public/assets/resources/images/products/tshirt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/target-nodejs-sdk-samples/36c6921e6479202aad1ed437a104b8713fd939f7/react-shopping-cart-demo/public/assets/resources/images/products/tshirt.png -------------------------------------------------------------------------------- /react-shopping-cart-demo/public/assets/resources/images/products/watch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/target-nodejs-sdk-samples/36c6921e6479202aad1ed437a104b8713fd939f7/react-shopping-cart-demo/public/assets/resources/images/products/watch.png -------------------------------------------------------------------------------- /react-shopping-cart-demo/public/assets/resources/images/target200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/target-nodejs-sdk-samples/36c6921e6479202aad1ed437a104b8713fd939f7/react-shopping-cart-demo/public/assets/resources/images/target200.png -------------------------------------------------------------------------------- /react-shopping-cart-demo/server.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const express = require("express"); 3 | const cookieParser = require("cookie-parser"); 4 | // Load the Target NodeJS SDK library 5 | const TargetClient = require("@adobe/target-nodejs-sdk"); 6 | // Load the Target configuration (replace with configurations specific to your Adobe Client & Org) 7 | const CONFIG = require("./config.json"); 8 | // Load the template of the HTML page returned in the response 9 | const TEMPLATE = fs.readFileSync(`${__dirname}/index.tpl`).toString(); 10 | 11 | // Initialize the Express app 12 | const app = express(); 13 | const PORT = process.env.PORT; 14 | 15 | // Enable TargetClient logging via the console logger 16 | const targetOptions = Object.assign({ logger: console }, CONFIG); 17 | // Create the TargetClient global instance 18 | const targetClient = TargetClient.create(targetOptions); 19 | 20 | // Setup cookie parsing middleware and static file serving from the /public directory 21 | app.use(cookieParser()); 22 | app.use(express.static(`${__dirname}/public`)); 23 | 24 | /** 25 | * Sets cookies in the response object 26 | * @param res response to be returned to the client 27 | * @param cookie cookie to be set 28 | */ 29 | function saveCookie(res, cookie) { 30 | if (!cookie) { 31 | return; 32 | } 33 | 34 | res.cookie(cookie.name, cookie.value, { maxAge: cookie.maxAge * 1000 }); 35 | } 36 | 37 | /** 38 | * Augments the HTML template with Target data and sends it to the client 39 | * @param res response object returned to the client 40 | * @param targetResponse response received from Target Delivery API 41 | */ 42 | function sendHtml(res, targetResponse) { 43 | // Set the serverState object, which at.js will use on the client-side to immediately apply Target offers 44 | const serverState = { 45 | request: targetResponse.request, 46 | response: targetResponse.response 47 | }; 48 | 49 | // Build the final HTML page by replacing the Target config and state placeholders with appropriate values 50 | let result = TEMPLATE.replace(/\$\{organizationId\}/g, CONFIG.organizationId) 51 | .replace("${clientCode}", CONFIG.client) 52 | .replace("${visitorState}", JSON.stringify(targetResponse.visitorState)) 53 | .replace("${serverState}", JSON.stringify(serverState, null, " ")); 54 | 55 | if (CONFIG.serverDomain) { 56 | result = result.replace("${serverDomain}", CONFIG.serverDomain); 57 | } else { 58 | result = result.replace('serverDomain: "${serverDomain}"', ""); 59 | } 60 | 61 | // Send the page to the client with a 200 OK HTTP status 62 | res.status(200).send(result); 63 | } 64 | 65 | /** 66 | * Returns the regular React app to the client, without any Target state, in case there was an error 67 | * with fetching Target data 68 | * @param res response object returned to the client 69 | */ 70 | function sendErrorHtml(res) { 71 | // Build the final HTML page, only Target config values are set in the template, for at.js initialization 72 | let result = TEMPLATE.replace(/\$\{organizationId\}/g, CONFIG.organizationId) 73 | .replace("${clientCode}", CONFIG.client) 74 | .replace("${visitorState}", "{}") 75 | .replace("${serverState}", "{}"); 76 | 77 | if (CONFIG.serverDomain) { 78 | result = result.replace("${serverDomain}", CONFIG.serverDomain); 79 | } else { 80 | result = result.replace('serverDomain: "${serverDomain}"', ""); 81 | } 82 | 83 | res.status(200).send(result); 84 | } 85 | 86 | /** 87 | * Return the headers to be set on the response object. 88 | * Note that Expires header is set to current Date, so the browser will always reload the page from the server 89 | * @returns {Object} response headers 90 | */ 91 | const getResponseHeaders = () => ({ 92 | "Content-Type": "text/html", 93 | Expires: new Date().toUTCString() 94 | }); 95 | 96 | /** 97 | * Sets headers and target cookies on the response, and sends the page with injected Target data back to the client 98 | * @param res response object to be returned to the client 99 | * @param targetResponse response received from Target Delivery API 100 | */ 101 | function sendResponse(res, targetResponse) { 102 | res.set(getResponseHeaders()); 103 | 104 | saveCookie(res, targetResponse.targetCookie); 105 | saveCookie(res, targetResponse.targetLocationHintCookie); 106 | sendHtml(res, targetResponse); 107 | } 108 | 109 | /** 110 | * Sets response headers and returns the page to the client, in case there was an error with fetching Target offers 111 | * @param res response object to be returned to the client 112 | */ 113 | function sendErrorResponse(res) { 114 | res.set(getResponseHeaders()); 115 | 116 | sendErrorHtml(res); 117 | } 118 | 119 | /** 120 | * Extract the Visitor, Target session and location hint cookies from the client request 121 | * and return these as Target Node Client options 122 | * @param req client request object 123 | * @returns {Object} Target and Visitor cookies as Target Node Client options map 124 | */ 125 | function getTargetCookieOptions(req) { 126 | return { 127 | visitorCookie: 128 | req.cookies[ 129 | encodeURIComponent( 130 | TargetClient.getVisitorCookieName(CONFIG.organizationId) 131 | ) 132 | ], 133 | targetCookie: req.cookies[TargetClient.TargetCookieName], 134 | targetLocationHintCookie: 135 | req.cookies[TargetClient.TargetLocationHintCookieName] 136 | }; 137 | } 138 | 139 | /** 140 | * If a Target Trace token was sent in the "authorization" client request query parameter, then we also set it 141 | * in the Target request 142 | * @param trace Target Delivery API request trace object 143 | * @param req client request object 144 | * @returns {Object} Target Delivery API request with the Trace token set from the original request query parameter 145 | */ 146 | function setTraceToken(request, req) { 147 | const { trace = {} } = request; 148 | 149 | if (Object.keys(trace).length === 0) { 150 | return; 151 | } 152 | 153 | const { authorizationToken = req.query.authorization } = trace; 154 | 155 | if (!authorizationToken || typeof authorizationToken !== "string") { 156 | return; 157 | } 158 | 159 | request.trace = Object.assign({}, trace, { authorizationToken }); 160 | } 161 | 162 | /** 163 | * Call the Target Node Client getOffers API asynchronously and send the response with Target offers back to the client 164 | * @param request Target Delivery API request 165 | * @param req client request object 166 | * @param res client response object 167 | */ 168 | async function processRequestWithTarget(request, req, res) { 169 | // Set the trace data on the Delivery API request object, if available 170 | setTraceToken(request, req); 171 | // Build Target Node.js SDK API getOffers options 172 | const options = Object.assign({ request }, getTargetCookieOptions(req)); 173 | 174 | try { 175 | // Call Target Node.js SDK getOffers asynchronously 176 | const resp = await targetClient.getOffers(options); 177 | // Send back the response with Target offers, getOffers call completes successfully 178 | sendResponse(res, resp); 179 | } catch (e) { 180 | // Alternatively, log the error and send the page without Target data 181 | console.error("AT error: ", e); 182 | sendErrorResponse(res); 183 | } 184 | } 185 | 186 | /** 187 | * Returns the Target request address, extracted from client request URL 188 | * @param req client request object 189 | * @returns {{url: *}} Target request address 190 | */ 191 | function getAddress(req) { 192 | return { url: req.headers.host + req.originalUrl }; 193 | } 194 | 195 | // Setup the root route Express app request handler for GET requests 196 | app.get("/", (req, res) => { 197 | // Build the Delivery API View Prefetch request 198 | const prefetchViewsRequest = { 199 | prefetch: { 200 | views: [{ address: getAddress(req) }] 201 | } 202 | }; 203 | 204 | // Process the request by calling Target Node.js SDK API 205 | processRequestWithTarget(prefetchViewsRequest, req, res); 206 | }); 207 | 208 | // Startup the Express server listener 209 | app.listen(PORT, () => console.log(`Server listening on port ${PORT}...`)); 210 | 211 | // Stop the server on any app warnings 212 | process.on("warning", e => { 213 | console.warn("Node application warning", e); 214 | process.exit(-1); 215 | }); 216 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/actions/addToCart.js: -------------------------------------------------------------------------------- 1 | import store from '../store' 2 | import axios from '../utils/mockAxios.js' 3 | import { fireActionTriggerCustomEvent } from '../utils/customEvents' 4 | 5 | import { fetchCart } from './fetchCart' 6 | /** 7 | * Create fetchAbout Action 8 | */ 9 | const requestAddToCart = () => { 10 | return { 11 | type: 'REQUEST_ADD_TO_CART' 12 | } 13 | }; 14 | 15 | const receiveAddToCart = (data) => { 16 | return { 17 | type: 'RECEIVE_ADD_TO_CART', 18 | payload: data 19 | } 20 | }; 21 | 22 | const checkUserOrGuest = (productId) => { 23 | if (localStorage.getItem('guest') || false) { 24 | return {id: productId, guestKey: localStorage.getItem('guest')} 25 | } 26 | }; 27 | 28 | export const addToCart = (productId, target) => { 29 | fireActionTriggerCustomEvent(target, { 30 | detail: { 31 | "linkName": target.getAttribute("data-link-name"), 32 | "action": target.getAttribute("data-track-action") 33 | } 34 | }); 35 | return dispatch => { 36 | dispatch(requestAddToCart()); 37 | return axios.post('cart', checkUserOrGuest(productId)) 38 | .then(response => response) 39 | .then(json => { 40 | dispatch(receiveAddToCart(json.data)); 41 | dispatch(fetchCart()) 42 | }); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/actions/addToWishlist.js: -------------------------------------------------------------------------------- 1 | import store from '../store' 2 | import axios from '../utils/mockAxios.js' 3 | import { fetchWishlist } from './fetchWishlist' 4 | import { fireActionTriggerCustomEvent } from '../utils/customEvents' 5 | 6 | /** 7 | * Create fetchAbout Action 8 | */ 9 | const requestAddToWishlist = () => { 10 | return { 11 | type: 'REQUEST_ADD_TO_WL' 12 | } 13 | }; 14 | 15 | const receiveAddToWishlist = (data) => { 16 | return { 17 | type: 'RECEIVE_ADD_TO_WL', 18 | payload: data 19 | } 20 | }; 21 | 22 | const checkUserOrGuest = (productId) => { 23 | if (localStorage.getItem('guest') || false) { 24 | return {id: productId, guestKey: localStorage.getItem('guest')} 25 | } 26 | }; 27 | 28 | export const addToWishlist = (productId, target) => { 29 | fireActionTriggerCustomEvent(target, { 30 | detail: { 31 | 'linkName': target.getAttribute('data-link-name'), 32 | 'action': target.getAttribute('data-track-action') 33 | } 34 | }); 35 | return dispatch => { 36 | dispatch(requestAddToWishlist()); 37 | return axios.post('wishlist', checkUserOrGuest(productId)) 38 | .then(response => response) 39 | .then(json => { 40 | dispatch(receiveAddToWishlist(json.data)); 41 | dispatch(fetchWishlist()); 42 | }); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/actions/deleteCart.js: -------------------------------------------------------------------------------- 1 | import store from '../store' 2 | import axios from '../utils/mockAxios.js' 3 | import {fetchCart} from './fetchCart' 4 | 5 | const requestDeleteCart = () => { 6 | return { 7 | type: 'REQUEST_DELETE_CART' 8 | } 9 | }; 10 | 11 | const receiveDeleteCart = () => { 12 | return { 13 | type: 'RECEIVE_DELETE_CART' 14 | } 15 | }; 16 | 17 | export const deleteCart = (key) => { 18 | return dispatch => { 19 | dispatch(requestDeleteCart()); 20 | return axios.deleteAll('cart') 21 | .then(response => response) 22 | .then(json => { 23 | dispatch(receiveDeleteCart()); 24 | dispatch(fetchCart()); 25 | }); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/actions/fetchAbout.js: -------------------------------------------------------------------------------- 1 | import store from '../store' 2 | import axios from '../utils/mockAxios.js' 3 | /** 4 | * Create fetchAbout Action 5 | */ 6 | const requestPosts = () => { 7 | return { 8 | type: 'REQUEST_ABOUT' 9 | } 10 | }; 11 | 12 | const receiveAbout = (data) => { 13 | return { 14 | type: 'RECEIVE_ABOUT', 15 | payload: data 16 | } 17 | }; 18 | 19 | export const fetchAbout = () => { 20 | return dispatch => { 21 | dispatch(requestPosts()); 22 | return axios.get('https://e-commerce-react-redux-demo.firebaseio.com/pages/about.json') 23 | .then(response => response) 24 | .then(json => { 25 | dispatch(receiveAbout(json.data)) 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/actions/fetchCart.js: -------------------------------------------------------------------------------- 1 | import store from '../store' 2 | import axios from '../utils/mockAxios.js' 3 | /** 4 | * Create fetchAbout Action 5 | */ 6 | const requestCart = () => { 7 | return { 8 | type: 'REQUEST_CART' 9 | } 10 | }; 11 | 12 | const receiveCart = (data) => { 13 | return { 14 | type: 'RECEIVE_CART', 15 | payload: (data === null) ? {} : data 16 | } 17 | }; 18 | 19 | export const fetchCart = () => { 20 | return dispatch => { 21 | dispatch(requestCart()); 22 | return axios.get('cart') 23 | .then(response => response) 24 | .then(json => { 25 | dispatch(receiveCart(json.data)); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/actions/fetchLatestProducts.js: -------------------------------------------------------------------------------- 1 | import store from '../store' 2 | import axios from 'axios' 3 | /** 4 | * Create fetchAbout Action 5 | */ 6 | const requestProducts = () => { 7 | return { 8 | type: 'REQUEST_LATEST_PRODUCTS' 9 | } 10 | }; 11 | 12 | const receiveProducts = (data) => { 13 | return { 14 | type: 'RECEIVE_LATEST_PRODUCTS', 15 | payload: data 16 | } 17 | }; 18 | 19 | export const fetchProducts = () => { 20 | return dispatch => { 21 | dispatch(requestProducts()); 22 | 23 | return axios.get(`assets/resources/data/latestProducts.json`) 24 | .then(json => { 25 | dispatch(receiveProducts(json.data)); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/actions/fetchProduct.js: -------------------------------------------------------------------------------- 1 | import store from '../store' 2 | import axios from 'axios' 3 | /** 4 | * Create fetchAbout Action 5 | */ 6 | const requestProduct = () => { 7 | return { 8 | type: 'REQUEST_PRODUCT' 9 | }; 10 | }; 11 | 12 | const receiveProduct = (data, id) => { 13 | return { 14 | type: 'RECEIVE_PRODUCT', 15 | payload: data, 16 | id: parseInt(id) 17 | }; 18 | }; 19 | 20 | export const fetchProduct = (id) => { 21 | return dispatch => { 22 | dispatch(requestProduct()); 23 | return axios.get(`assets/resources/data/products.json`) 24 | .then(response => response) 25 | .then(json => { 26 | dispatch(receiveProduct(json.data, id)); 27 | }); 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/actions/fetchProducts.js: -------------------------------------------------------------------------------- 1 | import store from '../store' 2 | import axios from 'axios' 3 | /** 4 | * Create fetchAbout Action 5 | */ 6 | const requestProducts = () => { 7 | return { 8 | type: 'REQUEST_PRODUCTS' 9 | } 10 | }; 11 | 12 | const receiveProducts = (data) => { 13 | return { 14 | type: 'RECEIVE_PRODUCTS', 15 | payload: data 16 | } 17 | }; 18 | 19 | export const fetchProducts = () => { 20 | return dispatch => { 21 | dispatch(requestProducts()); 22 | return axios.get(`assets/resources/data/products.json`) 23 | .then(json => { 24 | dispatch(receiveProducts(json.data)); 25 | }); 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/actions/fetchWishlist.js: -------------------------------------------------------------------------------- 1 | import store from '../store' 2 | import axios from '../utils/mockAxios.js' 3 | /** 4 | * Create fetchAbout Action 5 | */ 6 | const requestWishlist = () => { 7 | return { 8 | type: 'REQUEST_WL' 9 | } 10 | }; 11 | 12 | const receiveWishlist = (data) => { 13 | return { 14 | type: 'RECEIVE_WL', 15 | payload: (data === null) ? {} : data 16 | }; 17 | }; 18 | 19 | export const fetchWishlist = () => { 20 | return dispatch => { 21 | dispatch(requestWishlist()); 22 | return axios.get('wishlist') 23 | .then(response => response) 24 | .then(json => { 25 | dispatch(receiveWishlist(json.data)); 26 | }); 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/actions/removeFromCart.js: -------------------------------------------------------------------------------- 1 | import store from '../store' 2 | import axios from '../utils/mockAxios.js' 3 | import {fetchCart} from './fetchCart' 4 | 5 | const requestRemoveFromCart = () => { 6 | return { 7 | type: 'REQUEST_REMOVE_FROM_CART' 8 | }; 9 | }; 10 | 11 | const receiveRemoveFromCart = (data) => { 12 | return { 13 | type: 'RECEIVE_REMOVE_FROM_CART' 14 | }; 15 | }; 16 | 17 | export const removeFromCart = (key, target) => { 18 | return dispatch => { 19 | dispatch(requestRemoveFromCart()); 20 | return axios.delete('cart', key) 21 | .then(response => response) 22 | .then(json => { 23 | dispatch(receiveRemoveFromCart(json.data)); 24 | dispatch(fetchCart()) 25 | }); 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/actions/removeFromWishlist.js: -------------------------------------------------------------------------------- 1 | import store from '../store' 2 | import axios from '../utils/mockAxios.js' 3 | import {fetchWishlist} from './fetchWishlist' 4 | 5 | const requestRemoveFromWishlist = () => { 6 | return { 7 | type: 'REQUEST_REMOVE_FROM_WL' 8 | }; 9 | }; 10 | 11 | const receiveRemoveFromWishlist = (data) => { 12 | return { 13 | type: 'RECEIVE_REMOVE_FROM_WL' 14 | }; 15 | }; 16 | 17 | export const removeFromWishlist = (key, target) => { 18 | return dispatch => { 19 | dispatch(requestRemoveFromWishlist()); 20 | return axios.delete('wishlist', key) 21 | .then(response => response) 22 | .then(json => { 23 | dispatch(receiveRemoveFromWishlist(json.data)); 24 | dispatch(fetchWishlist()) 25 | }); 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/components/CartItem.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Link } from 'react-router-dom' 3 | /** 4 | * Create WishlistItem Component 5 | */ 6 | class CartItem extends Component { 7 | render() { 8 | return ( 9 | 10 | {this.props.product.id} 11 | {this.props.product.title} 12 | ${this.props.product.price} 13 | 14 | { 15 | e.preventDefault(); 16 | this.props.handleTrash(this.props.productKey); 17 | }}> 18 | 19 | 20 | 21 | 22 | ) 23 | } 24 | } 25 | 26 | export default CartItem 27 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | /** 3 | * Create Footer Component 4 | */ 5 | class Footer extends Component { 6 | render() { 7 | return ( 8 |
9 |
10 | 23 |
24 |
25 | ) 26 | } 27 | } 28 | export default Footer 29 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/components/ProductItem.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Link } from 'react-router-dom' 3 | /** 4 | * Create ProductItem Component 5 | */ 6 | class ProductItem extends Component { 7 | 8 | checkWishlist(id) { 9 | let check = null; 10 | Object.keys(this.props.wishlist).map((key) => { 11 | if (this.props.wishlist[key].id == id) check = true 12 | }); 13 | return check 14 | } 15 | 16 | checkCart(id) { 17 | let check = null; 18 | Object.keys(this.props.cart).map((key) => { 19 | if (this.props.cart[key].id == id) check = true 20 | }); 21 | return check 22 | } 23 | 24 | getKeyByIdForWl(id) { 25 | let productKey = ''; 26 | Object.keys(this.props.wishlist).map((key) => { 27 | if (this.props.wishlist[key].id == id) productKey = key 28 | }); 29 | return productKey 30 | } 31 | 32 | getKeyByIdForCart(id) { 33 | let productKey = ''; 34 | Object.keys(this.props.cart).map((key) => { 35 | if (this.props.cart[key].id == id) productKey = key 36 | }); 37 | return productKey 38 | } 39 | 40 | render() { 41 | return ( 42 |
43 |
44 |
45 |
46 | 47 |
48 |
49 |
50 |
51 |
52 |

{this.props.product.title}

54 |
55 |
56 |
57 |

58 | Price: 59 | ${this.props.product.price} 60 |

61 |
62 | 74 | 86 |
87 |
88 |
89 | ) 90 | } 91 | } 92 | export default ProductItem 93 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/components/WishlistItem.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Link } from 'react-router-dom' 3 | /** 4 | * Create WishlistItem Component 5 | */ 6 | class WishlistItem extends Component { 7 | render() { 8 | return ( 9 | 10 | {this.props.product.id} 11 | {this.props.product.title} 12 | ${this.props.product.price} 13 | 14 | { 15 | e.preventDefault(); 16 | this.props.handleTrash(this.props.productKey); 17 | }}> 18 | 19 | 20 | 21 | 22 | ) 23 | } 24 | } 25 | 26 | export default WishlistItem 27 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/containers/About.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import { fetchAbout } from '../actions/fetchAbout' 4 | import Helmet from "react-helmet" 5 | 6 | /** 7 | * Create About Container 8 | */ 9 | class About extends Component { 10 | componentDidMount() { 11 | const { dispatch } = this.props 12 | } 13 | 14 | render() { 15 | return ( 16 |
17 | 18 |
19 |
20 |
21 |

About demo

22 |
23 |

Single page applications (SPAs) implemented on popular frameworks like React and Angular and custom 24 | implementation frameworks are the new norm for the websites of today's "modern web". We've been developing 25 | for SPAs and the modern web for a while now. Now, we are taking this ground-breaking research and applying 26 | it to the entirely new Visual Experience Composer for SPAs, giving marketers the agility and control they 27 | need to build rich, personalized experiences at scale, no matter what framework or architecture they use. 28 | Developers can do a one-time setup by including a single line of javascript code to enable their SPA 29 | websites for VEC. This first-of-a-kind product innovation enables non-technical marketers to experiment 30 | and personalize on popular SPA frameworks.

31 |
32 | 33 |

This website is built using React-Redux, one of the most popular frameworks these days. In this demo, we 34 | will see how we can create tests and personalize content on SPAs in a do-it-yourself fashion without 35 | continuous development dependencies.

36 |
37 | 38 |

To understand the methodology used in this site, please visit our Documentation page here.

41 |
42 |
43 |
44 |

45 | ReactJS and Redux are not Adobe proprietary technologies.

46 | Transactions on this site are not real.

47 | Forked from GitHub

48 |

49 |
50 |
51 | ) 52 | } 53 | } 54 | /** 55 | * Insert Props Into Component 56 | */ 57 | const stateProps = (state) => { 58 | return { 59 | about: state.AboutReducer.data 60 | } 61 | }; 62 | 63 | export default connect(stateProps)(About) 64 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/containers/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import NavBar from './Navbar' 3 | import Footer from '../components/Footer' 4 | import Helmet from "react-helmet" 5 | import { connect } from 'react-redux' 6 | import { withRouter } from 'react-router' 7 | 8 | /** 9 | * Create App Component 10 | */ 11 | class App extends Component { 12 | render() { 13 | return ( 14 |
15 |
16 | 17 | 18 | {this.props.children} 19 |
20 |
21 |
22 | ) 23 | } 24 | } 25 | 26 | const stateProps = (state) => { 27 | return { 28 | loading: state.LoadingReducer.isVisible 29 | } 30 | }; 31 | 32 | export default withRouter(connect(stateProps)(App)) 33 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/containers/Cart.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import Helmet from "react-helmet" 4 | import { fetchProducts } from '../actions/fetchProducts' 5 | import { fetchCart } from '../actions/fetchCart' 6 | import { removeFromCart } from '../actions/removeFromCart' 7 | import CartItem from '../components/CartItem' 8 | import { Link } from 'react-router-dom' 9 | 10 | /** 11 | * Create Cart Container 12 | */ 13 | class Cart extends Component { 14 | getItemById(id) { 15 | let obj = {}; 16 | this.props.products.map((item) => { 17 | if (item.id == id) obj = item 18 | }); 19 | return obj; 20 | } 21 | 22 | handleTrash(key) { 23 | const { dispatch } = this.props; 24 | dispatch(removeFromCart(key)) 25 | } 26 | 27 | totalPricesArray() { 28 | let cartItems = this.props.cart; 29 | let getPricesById = (id) => { 30 | return this.getItemById(id).price 31 | }; 32 | let prices = []; 33 | Object.keys(this.props.cart).map(function (key) { 34 | prices.push(getPricesById(cartItems[key].id)) 35 | }); 36 | return prices; 37 | } 38 | 39 | componentDidMount() { 40 | const { dispatch } = this.props; 41 | dispatch(fetchProducts()); 42 | dispatch(fetchCart()); 43 | adobe.target.triggerView('cart'); 44 | } 45 | 46 | componentDidUpdate() { 47 | adobe.target.triggerView('cart', {page: false}); 48 | } 49 | 50 | render() { 51 | let total = this.totalPricesArray().reduce(function (prev, next) { 52 | return prev + next; 53 | }, 0); 54 | let checkoutButton = total > 0 ? 55 | Checkout your order : ""; 56 | 57 | return ( 58 |
59 | 60 |
61 |
62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | { 81 | Object.keys(this.props.cart).map((key) => { 82 | return () 84 | }) 85 | } 86 | 87 |
IDTitlePrice 
  Total: ${total} 
88 | {checkoutButton} 89 |
90 |
91 |
92 | ) 93 | } 94 | } 95 | /** 96 | * Insert Props Into Component 97 | */ 98 | const stateProps = (state) => { 99 | return { 100 | cart: state.CartReducer.data, 101 | products: state.ProductsReducer.data 102 | } 103 | }; 104 | 105 | export default connect(stateProps)(Cart) 106 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/containers/Confirm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Helmet from "react-helmet" 3 | import { generate } from 'randomstring' 4 | 5 | class Confirm extends Component { 6 | 7 | componentDidMount() { 8 | //digitalData.page.pageInfo.pageID = "order-confirm-page"; 9 | //digitalData.page.pageInfo.pageName = "ACS Demo - Order Confirmation Page"; 10 | } 11 | 12 | render() { 13 | return ( 14 |
15 | 16 |
17 |
18 |
19 |

Order Confirmation

20 |
21 |

Thank you for your order.

22 | 23 |

Order Number: {generate(10)}

24 |
25 |
26 |
27 | ) 28 | } 29 | } 30 | 31 | export default (Confirm) 32 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/containers/Home.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import ProductItem from '../components/ProductItem' 4 | import { fetchProducts } from '../actions/fetchLatestProducts' 5 | import { addToCart } from '../actions/addToCart' 6 | import { addToWishlist } from '../actions/addToWishlist' 7 | import { removeFromWishlist } from '../actions/removeFromWishlist' 8 | import { removeFromCart } from '../actions/removeFromCart' 9 | import Helmet from "react-helmet" 10 | import Slider from "react-slick" 11 | 12 | /** 13 | * Create ProductList Container 14 | */ 15 | class ProductList extends Component { 16 | 17 | addToCart(id, target) { 18 | const { dispatch } = this.props; 19 | dispatch(addToCart(id, target)) 20 | } 21 | 22 | addToWishlist(id, target) { 23 | const { dispatch } = this.props; 24 | dispatch(addToWishlist(id, target)) 25 | } 26 | 27 | removeFromWishlist(id, target) { 28 | const { dispatch } = this.props; 29 | dispatch(removeFromWishlist(id, target)) 30 | } 31 | 32 | removeFromCart(id, target) { 33 | const { dispatch } = this.props; 34 | dispatch(removeFromCart(id, target)) 35 | } 36 | 37 | componentDidMount() { 38 | const { dispatch } = this.props; 39 | dispatch(fetchProducts()) 40 | } 41 | 42 | render() { 43 | 44 | const settings = { 45 | dots: true, 46 | speed: 500, 47 | autoplay: false, 48 | autoplaySpeed: 3000, 49 | infinite: true 50 | }; 51 | 52 | return ( 53 | 54 |
55 | 56 |
57 |
58 | 59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |



67 | 68 |
69 |
70 |

Latest Products for 2019

71 | 72 |
73 | {this.props.products.map((product) => { 74 | return 83 | })} 84 |
85 |
86 |
87 |
88 |
89 | 90 | ) 91 | } 92 | } 93 | /** 94 | * Insert Props Into Component 95 | */ 96 | const stateProps = (state) => { 97 | return { 98 | products: state.LatestProductsReducer.data, 99 | wishlist: state.WishlistReducer.data, 100 | cart: state.CartReducer.data 101 | } 102 | }; 103 | export default connect(stateProps)(ProductList) 104 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/containers/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import { Link } from 'react-router-dom' 4 | import { fetchCart } from '../actions/fetchCart' 5 | import { fetchWishlist } from '../actions/fetchWishlist' 6 | import Cart from './Cart' 7 | import Wishlist from './Wishlist' 8 | 9 | /** 10 | * Create NavBar Container 11 | */ 12 | class NavBar extends Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | showBar: false, 17 | isCartOpen: false, 18 | isWishListOpen: false 19 | }; 20 | this.registerListeners(); 21 | } 22 | 23 | registerListeners() { 24 | document.onclick = evt => { 25 | const btns = document.querySelector('.cart-buttons'); 26 | const popover = document.querySelector('.cart-popover'); 27 | if (btns && btns.contains(evt.target) || popover && popover.contains(evt.target)) { 28 | evt.stopPropagation(); 29 | } else { 30 | this.closePopovers(); 31 | } 32 | }; 33 | } 34 | 35 | closePopovers() { 36 | this.setState({ 37 | isCartOpen: false, 38 | isWishListOpen: false 39 | }); 40 | } 41 | 42 | UNSAFE_componentWillMount() { 43 | const { dispatch } = this.props; 44 | dispatch(fetchCart()); 45 | dispatch(fetchWishlist()) 46 | } 47 | 48 | toggleNavBar() { 49 | this.setState({ 50 | showBar: !this.state.showBar 51 | }) 52 | } 53 | 54 | render() { 55 | return ( 56 |
57 | 94 |

95 | 96 |
97 | This website demonstrates how you can use Adobe Target library AT.js to author and deliver experiences on 98 | websites built with Single Page Apps (SPAs). 99 |
100 |

101 | 102 | {this.state.isCartOpen && ( 103 |
104 | this.closePopovers()}/> 105 |
106 | )} 107 | 108 | {this.state.isWishListOpen && ( 109 |
110 | this.closePopovers()} /> 111 |
112 | )} 113 |
114 | 115 | ) 116 | } 117 | 118 | toggleCartPopover() { 119 | const cartItemsCount = Object.keys(this.props.cart).length; 120 | this.setState({ 121 | isCartOpen: !this.state.isCartOpen && cartItemsCount > 0, 122 | isWishListOpen: false 123 | }); 124 | } 125 | 126 | toggleWishListPopover() { 127 | const wishlistItemsCount = Object.keys(this.props.wishlist).length; 128 | this.setState({ 129 | isWishListOpen: !this.state.isWishListOpen && wishlistItemsCount > 0, 130 | isCartOpen: false 131 | }); 132 | } 133 | } 134 | /** 135 | * Insert Props Into Component 136 | */ 137 | const stateProps = (state) => { 138 | return { 139 | cart: state.CartReducer.data, 140 | wishlist: state.WishlistReducer.data 141 | } 142 | }; 143 | 144 | export default connect(stateProps)(NavBar) 145 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/containers/Products.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/target-nodejs-sdk-samples/36c6921e6479202aad1ed437a104b8713fd939f7/react-shopping-cart-demo/src/containers/Products.js -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/containers/SingleProduct.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import Helmet from "react-helmet" 4 | import { fetchProduct } from '../actions/fetchProduct' 5 | import { addToCart } from '../actions/addToCart' 6 | import { addToWishlist } from '../actions/addToWishlist' 7 | import { removeFromWishlist } from '../actions/removeFromWishlist' 8 | import { removeFromCart } from '../actions/removeFromCart' 9 | 10 | /** 11 | * Create SingleProduct Container 12 | */ 13 | class SingleProduct extends Component { 14 | componentDidMount() { 15 | const { dispatch } = this.props; 16 | dispatch(fetchProduct(this.props.match.params.id)) 17 | } 18 | 19 | checkWishlist(id) { 20 | let check = null; 21 | Object.keys(this.props.wishlist).map((key) => { 22 | if (this.props.wishlist[key].id == id) check = true 23 | }); 24 | return check 25 | } 26 | 27 | checkCart(id) { 28 | let check = null; 29 | Object.keys(this.props.cart).map((key) => { 30 | if (this.props.cart[key].id == id) check = true 31 | }); 32 | return check 33 | } 34 | 35 | getKeyByIdForWl(id) { 36 | let productKey = ''; 37 | Object.keys(this.props.wishlist).map((key) => { 38 | if (this.props.wishlist[key].id == id) productKey = key 39 | }); 40 | return productKey 41 | } 42 | 43 | getKeyByIdForCart(id) { 44 | let productKey = ''; 45 | Object.keys(this.props.cart).map((key) => { 46 | if (this.props.cart[key].id == id) productKey = key 47 | }); 48 | return productKey 49 | } 50 | 51 | render() { 52 | return ( 53 |
54 | 65 |
66 |
67 |
68 |
69 | 70 |
71 | 72 |
73 |

{this.props.product.title}

74 | 75 |

Price: ${this.props.product.price}

76 |
77 | 78 |

{this.props.product.description}

79 | 90 | 100 |
101 | 102 |
103 |
104 |
105 |
106 | ) 107 | } 108 | } 109 | /** 110 | * Insert Props Into Component 111 | */ 112 | const stateProps = (state) => { 113 | return { 114 | cart: state.CartReducer.data, 115 | wishlist: state.WishlistReducer.data, 116 | product: state.ProductReducer.data 117 | } 118 | }; 119 | export default connect(stateProps)(SingleProduct) 120 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/containers/Wishlist.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import Helmet from "react-helmet" 4 | import { fetchWishlist } from '../actions/fetchWishlist' 5 | import { fetchProducts } from '../actions/fetchProducts' 6 | import WishlistItem from '../components/WishlistItem' 7 | import { removeFromWishlist } from '../actions/removeFromWishlist' 8 | import { Link } from 'react-router-dom' 9 | 10 | /** 11 | * Create Wishlist Container 12 | */ 13 | class Wishlist extends Component { 14 | getItemById(id) { 15 | let obj = {}; 16 | this.props.products.map((item) => { 17 | if (item.id == id) obj = item 18 | }); 19 | return obj 20 | } 21 | 22 | handleTrash(key) { 23 | const { dispatch } = this.props; 24 | dispatch(removeFromWishlist(key)) 25 | } 26 | 27 | componentDidMount() { 28 | const { dispatch } = this.props; 29 | dispatch(fetchProducts()); 30 | dispatch(fetchWishlist()); 31 | adobe.target.triggerView('cart'); 32 | } 33 | 34 | render() { 35 | return ( 36 |
37 | 38 |
39 |
40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | { 51 | Object.keys(this.props.wishlist).map((key) => { 52 | return () 54 | }) 55 | } 56 | 57 |
IDTitlePrice 
58 |
59 |
60 |
61 | ) 62 | } 63 | } 64 | /** 65 | * Insert Props Into Component 66 | */ 67 | const stateProps = (state) => { 68 | return { 69 | wishlist: state.WishlistReducer.data, 70 | products: state.ProductsReducer.data 71 | } 72 | }; 73 | 74 | export default connect(stateProps)(Wishlist) 75 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './containers/App' 4 | import About from './containers/About' 5 | import Cart from './containers/Cart' 6 | import Wishlist from './containers/Wishlist' 7 | import Home from './containers/Home' 8 | import SingleProduct from './containers/SingleProduct' 9 | import Checkout from './containers/Checkout' 10 | import Confirm from './containers/Confirm' 11 | import Products from './containers/Products' 12 | import { Provider } from 'react-redux' 13 | import store from './store' 14 | import { HashRouter, Route } from 'react-router-dom' 15 | import { syncHistoryWithStore } from 'react-router-redux' 16 | import { createBrowserHistory } from 'history'; 17 | 18 | function targetView() { 19 | var viewName = window.location.hash; 20 | 21 | // Sanitize viewName to get rid of any trailing symbols derived from URL 22 | if (viewName.startsWith('#')) { 23 | viewName = viewName.substr(1); 24 | } 25 | if (viewName.startsWith('/')) { 26 | viewName = viewName.substr(1); 27 | } 28 | 29 | viewName = viewName || 'home'; // view name cannot be empty 30 | 31 | // Validate if the Target Libraries are available on your website 32 | if (typeof adobe != 'undefined' && adobe.target && typeof adobe.target.triggerView === 'function') { 33 | adobe.target.triggerView(viewName); 34 | } 35 | } 36 | 37 | const history = syncHistoryWithStore(createBrowserHistory(), store); 38 | history.listen(targetView); 39 | 40 | /** 41 | * Render App 42 | */ 43 | ReactDOM.render( 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | store.dispatch({type:'CLEAR_PRODUCT'}) }/> 55 | 56 | 57 | , 58 | document.getElementById('app') 59 | ); 60 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/middlewares/Auth.js: -------------------------------------------------------------------------------- 1 | import { generate } from 'randomstring' 2 | 3 | export const Auth = store => next => action => { 4 | 5 | let prev = next(action); 6 | 7 | let filterUserData = (data) => { 8 | let newCart = {}; 9 | let existingGuest = localStorage.getItem('guest'); 10 | 11 | if (existingGuest) { 12 | Object.keys(data).map((key) => { 13 | if (data[key].guestKey == existingGuest) { 14 | newCart[key] = data[key] 15 | } 16 | }); 17 | } else { 18 | //create random key for guest 19 | localStorage.setItem('guest', generate(7)) 20 | } 21 | 22 | return newCart 23 | }; 24 | 25 | switch (action.type) { 26 | case 'RECEIVE_CART': 27 | next({type: 'RECEIVE_CART', payload: filterUserData(action.payload)}); 28 | break; 29 | case 'RECEIVE_WL': 30 | next({type: 'RECEIVE_WL', payload: filterUserData(action.payload)}); 31 | break; 32 | default: 33 | 34 | } 35 | 36 | return prev 37 | }; -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/middlewares/Loading.js: -------------------------------------------------------------------------------- 1 | export const Loading = store => next => action => { 2 | 3 | let prev = next(action); 4 | 5 | let isFetching = null; 6 | 7 | Object.keys(store.getState()).map((reducer) => { 8 | if ('fetching' in store.getState()[reducer]) { 9 | isFetching = isFetching || store.getState()[reducer]['fetching'] 10 | } 11 | }); 12 | 13 | if (isFetching) { 14 | next({type: "SHOW_LOADING"}) 15 | } else { 16 | next({type: "HIDE_LOADING"}) 17 | } 18 | 19 | return prev 20 | }; 21 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/reducers/AboutReducer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * About Reducer 3 | * @param {Object} state 4 | * @param {Object} action 5 | */ 6 | const AboutReducer = (state = {fetching: false, data: {title: '', content: ''}}, action) => { 7 | switch (action.type) { 8 | case 'REQUEST_ABOUT': 9 | return Object.assign({}, state, { 10 | fetching: true, 11 | data: {title: '', content: ''} 12 | }); 13 | break; 14 | case 'RECEIVE_ABOUT': 15 | return Object.assign({}, state, { 16 | fetching: false, 17 | data: action.payload 18 | }); 19 | break; 20 | default: 21 | return state 22 | } 23 | }; 24 | export default AboutReducer -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/reducers/CartReducer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cart Reducer 3 | * @param {Object} state 4 | * @param {Object} action 5 | */ 6 | const CartReducer = (state = { 7 | loading: false, 8 | data: {} 9 | }, action) => { 10 | switch (action.type) { 11 | case 'REQUEST_ADD_TO_CART': 12 | return Object.assign({}, state, {loading: true}); 13 | break; 14 | case 'RECEIVE_ADD_TO_CART': 15 | return Object.assign({}, state, {loading: false}); 16 | break; 17 | case 'REQUEST_CART': 18 | return Object.assign({}, state, {loading: true}); 19 | break; 20 | case 'RECEIVE_CART': 21 | return Object.assign({}, state, { 22 | loading: false, 23 | data: action.payload 24 | }); 25 | break; 26 | case 'REQUEST_REMOVE_FROM_CART': 27 | return Object.assign({}, state, { 28 | loading: true 29 | }); 30 | break; 31 | case 'RECEIVE_REMOVE_FROM_CART': 32 | return Object.assign({}, state, { 33 | loading: false 34 | }); 35 | break; 36 | case 'REQUEST_DELETE_CART': 37 | return Object.assign({}, state, { 38 | loading: true 39 | }); 40 | break; 41 | case 'RECEIVE_DELETE_CART': 42 | return Object.assign({}, state, { 43 | loading: false 44 | }); 45 | break; 46 | default: 47 | return state 48 | } 49 | }; 50 | export default CartReducer 51 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/reducers/LatestProductsReducer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Products Reducer 3 | * @param {Array} state 4 | * @param {Object} action 5 | */ 6 | const LatestProductsReducer = (state = {fetching: false, data: []}, action) => { 7 | switch (action.type) { 8 | case 'REQUEST_LATEST_PRODUCTS': 9 | return Object.assign({}, state, { 10 | fetching: true, 11 | data: [] 12 | }); 13 | break; 14 | 15 | case 'RECEIVE_LATEST_PRODUCTS': 16 | return Object.assign({}, state, { 17 | fetching: false, 18 | data: action.payload 19 | }); 20 | break; 21 | default: 22 | return state; 23 | } 24 | }; 25 | 26 | export default LatestProductsReducer -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/reducers/LoadingReducer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Products Reducer 3 | * @param {Array} state 4 | * @param {Object} action 5 | */ 6 | const LoadingReducer = (state = {isVisible: false}, action) => { 7 | 8 | switch (action.type) { 9 | case 'SHOW_LOADING': 10 | return Object.assign({}, state, { 11 | isVisible: true 12 | }); 13 | break; 14 | case 'HIDE_LOADING': 15 | return Object.assign({}, state, { 16 | isVisible: false 17 | }); 18 | break; 19 | default: 20 | return state 21 | } 22 | }; 23 | export default LoadingReducer -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/reducers/ProductReducer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Product Reducer 3 | * @param {Array} state 4 | * @param {Object} action 5 | */ 6 | const ProductReducer = (state = {fetching: false, data: {}}, action) => { 7 | switch (action.type) { 8 | case 'REQUEST_PRODUCT': 9 | return Object.assign({}, state, { 10 | fetching: true, 11 | data: {} 12 | }); 13 | break; 14 | case 'RECEIVE_PRODUCT': 15 | let filtered = action.payload.filter((product) => { 16 | return product.id == action.id 17 | }); 18 | return Object.assign({}, state, { 19 | fetching: false, 20 | data: filtered[0] 21 | }); 22 | break; 23 | case 'CLEAR_PRODUCT': 24 | return Object.assign({}, state, { 25 | fetching: false, 26 | data: {} 27 | }); 28 | break; 29 | default: 30 | return state 31 | } 32 | }; 33 | export default ProductReducer -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/reducers/ProductsReducer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Products Reducer 3 | * @param {Array} state 4 | * @param {Object} action 5 | */ 6 | const ProductsReducer = (state = {fetching: false, data: []}, action) => { 7 | switch (action.type) { 8 | case 'REQUEST_PRODUCTS': 9 | return Object.assign({}, state, { 10 | fetching: true, 11 | data: [] 12 | }); 13 | break; 14 | case 'RECEIVE_PRODUCTS': 15 | return Object.assign({}, state, { 16 | fetching: false, 17 | data: action.payload 18 | }); 19 | break; 20 | default: 21 | return state 22 | } 23 | }; 24 | export default ProductsReducer -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/reducers/WishlistReducer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Wishlist Reducer 3 | * @param {Object} state 4 | * @param {Object} action 5 | */ 6 | const WishlistReducer = (state = { 7 | loading: false, 8 | data: {} 9 | }, action) => { 10 | switch (action.type) { 11 | case 'REQUEST_ADD_TO_WL': 12 | return Object.assign({}, state, {loading: true}); 13 | break; 14 | case 'RECEIVE_ADD_TO_WL': 15 | return Object.assign({}, state, {loading: false}); 16 | break; 17 | case 'REQUEST_WL': 18 | return Object.assign({}, state, {loading: true}); 19 | break; 20 | case 'RECEIVE_WL': 21 | return Object.assign({}, state, { 22 | loading: false, 23 | data: action.payload 24 | }); 25 | break; 26 | case 'REQUEST_REMOVE_FROM_WL': 27 | return Object.assign({}, state, { 28 | loading: true 29 | }); 30 | break; 31 | case 'RECEIVE_REMOVE_FROM_WL': 32 | return Object.assign({}, state, { 33 | loading: false 34 | }); 35 | break; 36 | default: 37 | return state 38 | } 39 | }; 40 | export default WishlistReducer -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import AboutReducer from './AboutReducer' 3 | import ProductsReducer from './ProductsReducer' 4 | import LatestProductsReducer from './LatestProductsReducer' 5 | import ProductReducer from './ProductReducer' 6 | import LoadingReducer from './LoadingReducer' 7 | import CartReducer from './CartReducer' 8 | import WishlistReducer from './WishlistReducer' 9 | import { routerReducer } from 'react-router-redux' 10 | 11 | /** 12 | * Combine Reducers In One Object 13 | */ 14 | export default combineReducers({ 15 | AboutReducer, 16 | ProductsReducer, 17 | LatestProductsReducer, 18 | ProductReducer, 19 | LoadingReducer, 20 | CartReducer, 21 | WishlistReducer, 22 | routing: routerReducer 23 | }) 24 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunk from 'redux-thunk' 3 | import reducers from './reducers/index' 4 | import { Loading } from './middlewares/Loading' 5 | import { Auth } from './middlewares/Auth' 6 | 7 | //const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 8 | /** 9 | * Create Middleware 10 | */ 11 | const middleware = applyMiddleware(thunk, Loading, Auth); 12 | /** 13 | * Create Store 14 | */ 15 | const store = createStore(reducers, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(), middleware); 16 | 17 | export default store 18 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/styles/sass/app.scss: -------------------------------------------------------------------------------- 1 | .loading-wrapper { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100vw; 6 | height: 100vh; 7 | z-index: 5; 8 | background: #fff; 9 | .loading { 10 | display: table; 11 | width: 100vw; 12 | height: 100vh; 13 | span { 14 | display: table-cell; 15 | vertical-align: middle; 16 | text-align: center; 17 | width: 100vw; 18 | height: 100vh; 19 | } 20 | } 21 | &.is-loading { 22 | visibility: visible; 23 | } 24 | &.is-loaded { 25 | -webkit-animation: hideLoading 1s forwards; 26 | -moz-animation: hideLoading 1s forwards; 27 | -o-animation: hideLoading 1s forwards; 28 | animation: hideLoading 1s forwards; 29 | } 30 | } 31 | 32 | .section { 33 | min-height: 400px; 34 | } 35 | 36 | .btn-margin { 37 | margin-top: 1.5rem; 38 | margin-right: 1.5rem; 39 | } 40 | 41 | .badge { 42 | font-size: 1.5rem; 43 | margin-top: 1rem; 44 | margin-bottom: 1rem; 45 | } 46 | 47 | .slick-next:before, .slick-prev:before { 48 | color: #e41f1f; 49 | } 50 | 51 | .notice { 52 | border-radius: 5px; 53 | -moz-border-radius: 5px; 54 | -webkit-border-radius: 5px; 55 | border: 2px solid #a41c23; 56 | padding: 0.3rem; 57 | margin-top: 0.5rem; 58 | margin-bottom: 0.3rem; 59 | background-color: #c5eff5; 60 | text-align: center; 61 | color: #4682b4; 62 | } 63 | 64 | .button.menu { 65 | background-color: #3273dc; 66 | border-color: #3273dc; 67 | color: #ffffff; 68 | 69 | } 70 | 71 | .button.menu:focus, .button.menu:hover { 72 | background-color: #cd1e27; 73 | border-color: #f1560c; 74 | color: #ffffff; 75 | } 76 | 77 | .section { 78 | padding: 0rem 1.5rem; 79 | } 80 | 81 | .nav-item { 82 | img { 83 | max-height: 4rem; 84 | } 85 | } 86 | 87 | .heading { 88 | text-transform: none; 89 | } 90 | 91 | body { 92 | &.isLoading { 93 | overflow: hidden; 94 | } 95 | } 96 | 97 | @-webkit-keyframes hideLoading { 98 | 0% { 99 | visibility: visible; 100 | } 101 | 100% { 102 | visibility: hidden; 103 | } 104 | } 105 | 106 | @-moz-keyframes hideLoading { 107 | 0% { 108 | visibility: visible; 109 | } 110 | 100% { 111 | visibility: hidden; 112 | } 113 | } 114 | 115 | @-o-keyframes hideLoading { 116 | 0% { 117 | visibility: visible; 118 | } 119 | 100% { 120 | visibility: hidden; 121 | } 122 | } 123 | 124 | @keyframes hideLoading { 125 | 0% { 126 | visibility: visible; 127 | } 128 | 100% { 129 | visibility: hidden; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/utils/customEvents.js: -------------------------------------------------------------------------------- 1 | const fireViewEndCustomEvent = () => { 2 | console.log("Firing Custom Event 'event-view-end'"); 3 | var event = new CustomEvent('event-view-end'); 4 | var obj = document.querySelector("#app"); 5 | obj.dispatchEvent(event); 6 | }; 7 | 8 | const fireViewStartCustomEvent = (data) => { 9 | console.log("Firing Custom Event 'event-view-start'"); 10 | let event = new CustomEvent('event-view-start', data); 11 | document.body.dispatchEvent(event); 12 | }; 13 | 14 | const fireActionTriggerCustomEvent = (target, data) => { 15 | console.log("Firing Custom Triggered Event"); 16 | var event = new CustomEvent('event-action-trigger', data); 17 | var obj = target.dispatchEvent(event); 18 | }; 19 | 20 | export { 21 | fireViewEndCustomEvent, 22 | fireViewStartCustomEvent, 23 | fireActionTriggerCustomEvent 24 | } 25 | -------------------------------------------------------------------------------- /react-shopping-cart-demo/src/utils/mockAxios.js: -------------------------------------------------------------------------------- 1 | export default { 2 | post: (entity, data) => { 3 | return new Promise((resolve) => { 4 | var entityCollection = JSON.parse(localStorage.getItem(entity)) || {}, 5 | key = Date.now(); 6 | entityCollection[Date.now()] = data; 7 | localStorage.setItem(entity, JSON.stringify(entityCollection)); 8 | resolve({data: {name: key}}); 9 | }); 10 | }, 11 | 12 | get: (entity) => { 13 | return new Promise((resolve) => { 14 | var entityCollection = JSON.parse(localStorage.getItem(entity)) || {}; 15 | resolve({data: entityCollection}); 16 | }); 17 | }, 18 | 19 | delete: (entity, key) => { 20 | return new Promise((resolve) => { 21 | var entityCollection = JSON.parse(localStorage.getItem(entity)) || {}; 22 | delete entityCollection[key]; 23 | localStorage.setItem(entity, JSON.stringify(entityCollection)); 24 | resolve({data: null}); 25 | }); 26 | }, 27 | 28 | deleteAll: (entity) => { 29 | return new Promise((resolve) => { 30 | localStorage.removeItem(entity); 31 | resolve({data: null}); 32 | }); 33 | } 34 | }; -------------------------------------------------------------------------------- /react-shopping-cart-demo/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | 4 | module.exports = { 5 | context: path.join(__dirname, './'), 6 | entry: './src/index.js', 7 | output: { 8 | path: path.join(__dirname, 'public', 'assets'), 9 | filename: 'js/app.js' 10 | }, 11 | resolve: { 12 | extensions: ['.js', '.jsx'] 13 | }, 14 | performance: { 15 | hints: false 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.(js|jsx)$/, 21 | exclude: /node_modules/, 22 | loader: 'babel-loader', 23 | query: { 24 | presets: ['es2015', 'react', 'env'] 25 | } 26 | }, 27 | { 28 | test: /\.(jpg|jpeg|gif|png)$/, 29 | exclude: /node_modules/, 30 | loader: 'url-loader?limit=1024&name=images/[name].[ext]' 31 | } 32 | ] 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /shared-ecid-analytics-integration/README.md: -------------------------------------------------------------------------------- 1 | # Shared ECID and Analytics integration sample 2 | 3 | This sample demonstrates the use-case of sharing of a common ECID Visitor instance across several 4 | Target client SDK `getOffers` calls. 5 | 6 | By default, the Target client SDK instantiates a new ECID Visitor instance internally, on each `getOffers()` 7 | / `sendNotifications()` call. However, when there's a need to share a single ECID Visitor instance, this can be 8 | achieved by explicitly instantiating one and setting it in the Target client SDK via a call to `setVisitor()` 9 | API method. Any subsequent calls to Target client SDK API will use the provided Visitor instance. 10 | In this case, there's also no need to provide the VisitorId cookie (`options.visitorCookie`) in calls to 11 | Target client SDK API methods, as it's only required for internal Visitor instantiation. 12 | 13 | In the provided sample the two Target calls are executed concurrently. 14 | Note the use of distinct `consumerIds` in each call, for proper subsequent stitching with Analytics. 15 | 16 | Also note that a single `getOffers()` call with multiple mboxes/views should generally be preferred over multiple 17 | `getOffers()` calls with a shared Visitor instance. 18 | 19 | ## Usage 20 | 1. Install dependencies: `npm i` 21 | 2. Start: `npm start` 22 | 23 | -------------------------------------------------------------------------------- /shared-ecid-analytics-integration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shared-ecid-analytics-integration", 3 | "version": "1.0.0", 4 | "description": "Adobe Target Node.js SDK, shared-ecid-analytics-integration sample", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:adobe/target-nodejs-sdk-samples.git" 12 | }, 13 | "keywords": [ 14 | "NodeJS", 15 | "Server", 16 | "API", 17 | "Adobe", 18 | "Target", 19 | "MCID", 20 | "ECID", 21 | "Visitor", 22 | "Delivery", 23 | "shared-ecid-analytics-integration" 24 | ], 25 | "author": "Adobe Systems Inc.", 26 | "license": "Apache-2.0", 27 | "dependencies": { 28 | "@adobe/target-nodejs-sdk": "^2.1.0", 29 | "cookie-parser": "^1.4.4", 30 | "express": "^4.17.1", 31 | "uuid": "^3.3.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /shared-ecid-analytics-integration/server.js: -------------------------------------------------------------------------------- 1 | /*************************************************************************************** 2 | * (c) 2019 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | ****************************************************************************************/ 12 | 13 | const fs = require("fs"); 14 | const express = require("express"); 15 | const cookieParser = require("cookie-parser"); 16 | const Visitor = require("@adobe-mcid/visitor-js-server"); 17 | const uuidv4 = require("uuid/v4"); 18 | const TargetClient = require("@adobe/target-nodejs-sdk"); 19 | const CONFIG = { 20 | client: "adobetargetmobile", 21 | organizationId: "B8A054D958807F770A495DD6@AdobeOrg", 22 | timeout: 10000, 23 | logger: console 24 | }; 25 | const targetClient = TargetClient.create(CONFIG); 26 | const TEMPLATE = fs.readFileSync(__dirname + "/templates/index.tpl").toString(); 27 | 28 | const app = express(); 29 | app.use(cookieParser()); 30 | app.use(express.static(__dirname + "/public")); 31 | 32 | function saveCookie(res, cookie) { 33 | if (!cookie) { 34 | return; 35 | } 36 | 37 | res.cookie(cookie.name, cookie.value, { maxAge: cookie.maxAge * 1000 }); 38 | } 39 | 40 | const getResponseHeaders = () => ({ 41 | "Content-Type": "text/html", 42 | Expires: new Date().toUTCString() 43 | }); 44 | 45 | function sendHtml(res, offer) { 46 | const htmlResponse = TEMPLATE.replace( 47 | "${organizationId}", 48 | CONFIG.organizationId 49 | ) 50 | .replace("${visitorState}", JSON.stringify(offer.visitorState)) 51 | .replace("${content}", JSON.stringify(offer, null, " ")); 52 | 53 | res.status(200).send(htmlResponse); 54 | } 55 | 56 | function sendSuccessResponse(res, response) { 57 | res.set(getResponseHeaders()); 58 | saveCookie(res, response.targetCookie); 59 | sendHtml(res, response); 60 | } 61 | 62 | function sendErrorResponse(res, error) { 63 | res.set(getResponseHeaders()); 64 | res.status(500).send(error); 65 | } 66 | 67 | function getAddress(req) { 68 | return { url: req.headers.host + req.originalUrl }; 69 | } 70 | 71 | app.get("/", async (req, res) => { 72 | const visitorCookie = 73 | req.cookies[ 74 | encodeURIComponent( 75 | TargetClient.getVisitorCookieName(CONFIG.organizationId) 76 | ) 77 | ]; 78 | const visitor = new Visitor(CONFIG.organizationId, visitorCookie); 79 | const sessionId = uuidv4(); 80 | 81 | const targetCookie = req.cookies[TargetClient.TargetCookieName]; 82 | const firstRequest = { 83 | execute: { 84 | mboxes: [ 85 | { 86 | address: getAddress(req), 87 | name: "a1-serverside-ab" 88 | } 89 | ] 90 | } 91 | }; 92 | const secondRequest = { 93 | execute: { 94 | mboxes: [ 95 | { 96 | address: getAddress(req), 97 | name: "a1-serverside-ab" 98 | }, 99 | { 100 | address: getAddress(req), 101 | name: "a1-serverside-xt" 102 | } 103 | ] 104 | } 105 | }; 106 | 107 | try { 108 | const firstTargetRequest = targetClient.getOffers({ 109 | request: firstRequest, 110 | targetCookie, 111 | sessionId, 112 | visitor, 113 | consumerId: "first" 114 | }); 115 | const secondTargetRequest = targetClient.getOffers({ 116 | request: secondRequest, 117 | targetCookie, 118 | sessionId, 119 | visitor, 120 | consumerId: "second" 121 | }); 122 | const firstResponse = await firstTargetRequest; 123 | const secondResponse = await secondTargetRequest; 124 | const response = { 125 | firstOffer: firstResponse, 126 | secondOffer: secondResponse, 127 | targetCookie: secondResponse.targetCookie, 128 | visitorState: secondResponse.visitorState 129 | }; 130 | sendSuccessResponse(res, response); 131 | } catch (error) { 132 | console.error("Target:", error); 133 | sendErrorResponse(res, error); 134 | } 135 | }); 136 | 137 | app.listen(3000, function() { 138 | console.log("Listening on port 3000 and watching!"); 139 | }); 140 | -------------------------------------------------------------------------------- /shared-ecid-analytics-integration/templates/index.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Shared ECID (Visitor API) with Analytics Integration Sample 6 | 7 | 10 | 11 | 12 |
${content}
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /target-only/README.md: -------------------------------------------------------------------------------- 1 | # Target only sample 2 | 3 | The Target Node.js SDK can be used to retrieve personalized content from Target without forcing the use of ECID. 4 | 5 | ```js 6 | const TargetClient = require("@adobe/target-nodejs-sdk"); 7 | 8 | const targetClient = TargetClient.create({ 9 | client: "acmeclient", 10 | organizationId: "1234567890@AdobeOrg" 11 | }); 12 | 13 | const request = { 14 | execute: { 15 | mboxes: [{ 16 | name: "a1-serverside-ab" 17 | }] 18 | } 19 | }; 20 | 21 | try { 22 | const response = await targetClient.getOffers({ request }); 23 | console.log('Response', response); 24 | } catch (error) { 25 | console.error('Error', error); 26 | } 27 | ``` 28 | 29 | By default, the Target Node.js SDK generates a new session ID for every Target call, 30 | which might not always be the desired behavior. To ensure that Target properly tracks the user session, 31 | you should ensure that the Target cookie is sent to the browser after Target content is retrieved 32 | and the Target cookie value is passed to `getOffers()`/`sendNotifications()` as an incoming request is processed. 33 | The original request URL should also be passed in the `address` field of the Delivery request. 34 | 35 | ## Usage 36 | 1. Install dependencies: `npm i` 37 | 2. Start: `npm start` 38 | -------------------------------------------------------------------------------- /target-only/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "target-only", 3 | "version": "1.0.0", 4 | "description": "Adobe Target Node.js SDK, target-only sample", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:adobe/target-nodejs-sdk-samples.git" 12 | }, 13 | "keywords": [ 14 | "NodeJS", 15 | "Server", 16 | "API", 17 | "Adobe", 18 | "Target", 19 | "MCID", 20 | "ECID", 21 | "Visitor", 22 | "Delivery", 23 | "target-only" 24 | ], 25 | "author": "Adobe Systems Inc.", 26 | "license": "Apache-2.0", 27 | "dependencies": { 28 | "@adobe/target-nodejs-sdk": "^2.1.0", 29 | "cookie-parser": "^1.4.4", 30 | "express": "^4.17.1" 31 | }, 32 | "devDependencies": {} 33 | } 34 | -------------------------------------------------------------------------------- /target-only/server.js: -------------------------------------------------------------------------------- 1 | /*************************************************************************************** 2 | * (c) 2019 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | ****************************************************************************************/ 12 | 13 | const fs = require("fs"); 14 | const express = require("express"); 15 | const cookieParser = require("cookie-parser"); 16 | const TargetClient = require("@adobe/target-nodejs-sdk"); 17 | const CONFIG = { 18 | client: "adobetargetmobile", 19 | organizationId: "B8A054D958807F770A495DD6@AdobeOrg", 20 | timeout: 10000, 21 | logger: console 22 | }; 23 | const targetClient = TargetClient.create(CONFIG); 24 | const TEMPLATE = fs.readFileSync(__dirname + "/templates/index.tpl").toString(); 25 | 26 | const app = express(); 27 | app.use(cookieParser()); 28 | 29 | function saveCookie(res, cookie) { 30 | if (!cookie) { 31 | return; 32 | } 33 | 34 | res.cookie(cookie.name, cookie.value, {maxAge: cookie.maxAge * 1000}); 35 | } 36 | 37 | const getResponseHeaders = () => ({ 38 | "Content-Type": "text/html", 39 | "Expires": new Date().toUTCString() 40 | }); 41 | 42 | function sendHtml(res, offer) { 43 | const htmlResponse = TEMPLATE.replace("${content}", JSON.stringify(offer, null, ' ')); 44 | 45 | res.status(200).send(htmlResponse); 46 | } 47 | 48 | function sendSuccessResponse(res, response) { 49 | res.set(getResponseHeaders()); 50 | saveCookie(res, response.targetCookie); 51 | sendHtml(res, response); 52 | } 53 | 54 | function sendErrorResponse(res, error) { 55 | res.set(getResponseHeaders()); 56 | res.status(500).send(error); 57 | } 58 | 59 | function getAddress(req) { 60 | return { url: req.headers.host + req.originalUrl } 61 | } 62 | 63 | app.get("/", async (req, res) => { 64 | const targetCookie = req.cookies[TargetClient.TargetCookieName]; 65 | const request = { 66 | execute: { 67 | mboxes: [{ 68 | address: getAddress(req), 69 | name: "a1-serverside-ab" 70 | }] 71 | }}; 72 | 73 | try { 74 | const response = await targetClient.getOffers({ request, targetCookie }); 75 | sendSuccessResponse(res, response); 76 | } catch (error) { 77 | console.error("Target:", error); 78 | sendErrorResponse(res, error); 79 | } 80 | }); 81 | 82 | app.listen(3000, function () { 83 | console.log("Listening on port 3000 and watching!"); 84 | }); 85 | -------------------------------------------------------------------------------- /target-only/templates/index.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Target Only Sample 6 | 7 | 8 |
${content}
9 | 10 | 11 | -------------------------------------------------------------------------------- /target-traces/README.md: -------------------------------------------------------------------------------- 1 | # Target traces sample 2 | 3 | ## Usage 4 | 1. Install dependencies: `npm i` 5 | 2. Start: `npm start` 6 | 7 | ## How it works 8 | Target Node.js SDK allows you to send a trace token that will be used by Target edge to generate execution traces. 9 | These traces can then be exposed in the page context, so Chrome extensions or other tools could display trace details. 10 | 11 | The current sample looks for trace token in a query string parameter named `authorization` and the trace data is 12 | placed into a JavaScript object named `window.___target_traces`. 13 | 14 | In order to generate a trace token please follow these steps: 15 | 1. Login to Experience Cloud 16 | 2. Launch the Target UI 17 | 3. In Target UI top navigation bar select `Setup` 18 | 4. Next, from the left navigation bar select `Implementation` 19 | 5. At the bottom of the page you should find `Debugger Tools` settings 20 | 6. Click on the `Generate Authentication Token` button 21 | 22 | Once you have the trace token and the sample app is running to see everything in action you should open: 23 | [http://localhost:3000?authorization=YOUR_TRACE_TOKEN](http://localhost:3000?authorization=YOUR_TRACE_TOKEN) 24 | 25 | NOTE: make sure to replace `YOUR_TRACE_TOKEN` with the appropriate token value. 26 | -------------------------------------------------------------------------------- /target-traces/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "target-traces", 3 | "version": "1.0.0", 4 | "description": "Adobe Target Node.js SDK, target-traces sample", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:adobe/target-nodejs-sdk-samples.git" 12 | }, 13 | "keywords": [ 14 | "NodeJS", 15 | "Server", 16 | "API", 17 | "Adobe", 18 | "Target", 19 | "MCID", 20 | "ECID", 21 | "Visitor", 22 | "Delivery", 23 | "target-traces" 24 | ], 25 | "author": "Adobe Systems Inc.", 26 | "license": "Apache-2.0", 27 | "dependencies": { 28 | "@adobe/target-nodejs-sdk": "^2.1.0", 29 | "cookie-parser": "^1.4.4", 30 | "express": "^4.17.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /target-traces/server.js: -------------------------------------------------------------------------------- 1 | /*************************************************************************************** 2 | * (c) 2019 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | ****************************************************************************************/ 12 | 13 | const fs = require("fs"); 14 | const express = require("express"); 15 | const cookieParser = require("cookie-parser"); 16 | const TargetClient = require("@adobe/target-nodejs-sdk"); 17 | const CONFIG = { 18 | client: "adobetargetmobile", 19 | organizationId: "B8A054D958807F770A495DD6@AdobeOrg", 20 | timeout: 10000, 21 | logger: console 22 | }; 23 | const targetClient = TargetClient.create(CONFIG); 24 | const TEMPLATE = fs.readFileSync(__dirname + "/templates/index.tpl").toString(); 25 | 26 | const app = express(); 27 | app.use(cookieParser()); 28 | 29 | function saveCookie(res, cookie) { 30 | if (!cookie) { 31 | return; 32 | } 33 | 34 | res.cookie(cookie.name, cookie.value, {maxAge: cookie.maxAge * 1000}); 35 | } 36 | 37 | const getResponseHeaders = () => ({ 38 | "Content-Type": "text/html", 39 | "Expires": new Date().toUTCString() 40 | }); 41 | 42 | function sendHtml(res, offer) { 43 | const htmlResponse = TEMPLATE 44 | .replace("${content}", JSON.stringify(offer, null, ' ')) 45 | .replace("${trace}", JSON.stringify(offer.trace)); 46 | 47 | res.status(200).send(htmlResponse); 48 | } 49 | 50 | function sendSuccessResponse(res, response) { 51 | res.set(getResponseHeaders()); 52 | saveCookie(res, response.targetCookie); 53 | sendHtml(res, response); 54 | } 55 | 56 | function sendErrorResponse(res, error) { 57 | res.set(getResponseHeaders()); 58 | res.status(500).send(error); 59 | } 60 | 61 | function getAddress(req) { 62 | return { url: req.headers.host + req.originalUrl } 63 | } 64 | 65 | app.get("/", async (req, res) => { 66 | const targetCookie = req.cookies[TargetClient.TargetCookieName]; 67 | const request = { 68 | trace: { 69 | authorizationToken: req.query.authorization 70 | }, 71 | execute: { 72 | mboxes: [{ 73 | address: getAddress(req), 74 | name: "a1-serverside-ab" 75 | }] 76 | }}; 77 | 78 | try { 79 | const response = await targetClient.getOffers({ request, targetCookie }); 80 | sendSuccessResponse(res, response); 81 | } catch (error) { 82 | console.error("Target:", error); 83 | sendErrorResponse(res, error); 84 | } 85 | }); 86 | 87 | app.listen(3000, function () { 88 | console.log("Listening on port 3000 and watching!"); 89 | }); 90 | -------------------------------------------------------------------------------- /target-traces/templates/index.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Target Traces Sample 6 | 10 | 11 | 12 |
${content}
13 | 14 | 15 | --------------------------------------------------------------------------------