diff --git a/tools/htmloffer/README.md b/tools/htmloffer/README.md new file mode 100644 index 00000000..70fcb39b --- /dev/null +++ b/tools/htmloffer/README.md @@ -0,0 +1,810 @@ +# Adobe Target HTML Offer Integration + +This guide provides a complete step-by-step walkthrough for setting up Adobe Target HTML Offer integration with your EDS (Experience-Driven Sites) project. The integration allows content authors to export any block or section as an HTML Offer directly to Adobe Target for A/B testing and personalization campaigns. + +## Table of Contents + +1. [Overview](#overview) +2. [Prerequisites](#prerequisites) +3. [Setting up Adobe IO Project](#setting-up-adobe-io-project) +4. [Installing CLI and Testing Integration](#installing-cli-and-testing-integration) +5. [Creating Adobe IO Actions](#creating-adobe-io-actions) +6. [Deploying and Testing](#deploying-and-testing) +7. [EDS Project Implementation](#eds-project-implementation) +8. [Using HTML Offers in VEC Editor](#using-html-offers-in-vec-editor) +9. [Troubleshooting](#troubleshooting) +10. [References](#references) + +## Overview + +The Adobe Target HTML Offer integration consists of: + +- **Frontend**: Export dialog for content authors to select blocks/sections and export to Target +- **Backend**: Adobe IO Runtime actions for authentication and Target API integration +- **Automation**: Fragment ID system for identifying exportable content +- **Rendering**: Automatic event rebinding for Target-delivered content + +## Prerequisites + +- Adobe Experience Cloud account with Target access +- Admin access to Adobe Developer Console +- Node.js 18+ installed +- Access to your EDS project repository +- Basic understanding of Adobe Target and Adobe IO Runtime + +## Setting up Adobe IO Project + +### Step 1: Access Adobe Developer Console + +1. Go to [Adobe Developer Console](https://console.adobe.io/) +2. Sign in with your Adobe ID +3. Ensure your account has both **Product Admin** and **Developer** level access to Target + +### Step 2: Create New Project + +1. Select your Experience Cloud Organization +2. Click **Create new project** +3. Choose **Create an empty project** +4. Give it a meaningful name (e.g., "Target HTML Offer Integration") + +### Step 3: Add Adobe Target API + +1. Click **Add API** to add a REST API +2. Search for and select **Adobe Target** +3. Click **Next** + +### Step 4: Configure Service Account (JWT) + +1. Select **Option 1: Generate a key pair** +2. Click **Generate keypair** +3. **Download the `config` file** (contains your private key) +4. Click **Next** + +**⚠️ Important**: JWT credentials will be deprecated on January 1, 2025. Plan to migrate to OAuth Server-to-Server credentials. + +### Step 5: Select Product Profile + +1. Choose product profile(s) corresponding to your Target properties +2. If not using properties, select **Default Workspace** +3. Click **Save configured API** + +### Step 6: Create Integration + +1. Click **Create Integration** +2. Rename your project to something meaningful +3. Note your **Client ID** and **Client Secret** + +### Step 7: Configure JWT Scopes + +1. Go to **Service Account (JWT)** section +2. Add these required scopes: + ``` + openid + AdobeID + target_sdk + target_admin + target_write + target_read + additional_info.projectedProductContext + read_organizations + additional_info.roles + ``` + +## Installing CLI and Testing Integration + +### Step 1: Install Adobe IO CLI + +```bash +npm install -g @adobe/aio-cli +``` + +### Step 2: Login to Adobe IO + +```bash +aio login +``` + +### Step 3: Create Runtime Namespace + +```bash +# List existing namespaces +aio runtime namespace list + +# Create new namespace if needed +aio runtime namespace create +``` + +### Step 4: Test Authentication (Optional) + +You can test your authentication setup using Postman: + +1. **Export Project Details to Postman**: + - In Adobe Developer Console, go to your project's **Service Account (JWT)** section + - Click **Download for Postman** > **Service Account (JWT)** + - Import the JSON file into Postman + +2. **Generate Access Token**: + - Import the [Adobe I/O Access Token Generation Postman collection](https://experienceleague.adobe.com/en/docs/target-dev/developer/api/configure-authentication) + - Use the **IMS: JWT Generate + Auth via User Token** request + - Note: Token is valid for 24 hours + +3. **Test Target API**: + - Import the Adobe Target Admin APIs Postman Collection + - Use the **List activities** request to verify authentication + +## Creating Adobe IO Actions + +### Step 1: Project Structure + +Your Adobe IO project should have this structure: + +``` +aio/ +├── actions/ +│ ├── gettoken/ +│ │ └── index.js +│ └── exportoffers/ +│ └── index.js +├── app.config.yaml +└── package.json +``` + +### Step 2: Dependencies + +Ensure your `package.json` includes: + +```json +{ + "dependencies": { + "@adobe/aio-sdk": "^3.0.0", + "@adobe/aio-lib-files": "^3.0.0", + "@adobe/aio-lib-target": "^4.0.1", + "openwhisk": "^3.21.7" + } +} +``` + +### Step 3: Configuration + +Your `app.config.yaml` should look like: + +```yaml +application: + actions: actions + web: false + runtimeManifest: + packages: + sling-da: + license: Apache-2.0 + actions: + gettoken: + function: actions/gettoken/index.js + web: false + runtime: nodejs:18 + inputs: + ADOBE_CLIENT_ID: $SERVICE_API_KEY + ADOBE_CLIENT_SECRET: $ADOBE_CLIENT_SECRET + ADOBE_TARGET_SCOPES: $ADOBE_TARGET_SCOPES + LOG_LEVEL: info + annotations: + final: true + require-adobe-auth: false + limits: + timeout: 60000 + memorySize: 1024 + + exportoffers: + function: actions/exportoffers/index.js + web: true + runtime: nodejs:18 + inputs: + ADOBE_CLIENT_ID: $ADOBE_CLIENT_ID + ADOBE_TARGET_TENANT: 'your-tenant-id' + LOG_LEVEL: info + AIO_RUNTIME_NAMESPACE: $AIO_RUNTIME_NAMESPACE + ADOBE_TARGET_WORKSPACE_ID: 'your-workspace-id' + annotations: + final: true + require-adobe-auth: false + limits: + timeout: 60000 + memorySize: 1024 +``` + +### Step 4: Get Token Action + +Create `aio/actions/gettoken/index.js`: + +```javascript +const { Core } = require('@adobe/aio-sdk'); +const fetch = require('node-fetch'); +const { errorResponse } = require('../utils'); + +const TOKEN_CACHE_KEY = 'adobe_access_token'; + +async function main(params) { + const logger = Core.Logger('gettoken', { level: params.LOG_LEVEL || 'info' }); + + try { + if (!params.ADOBE_CLIENT_ID || !params.ADOBE_CLIENT_SECRET) { + throw new Error('Missing required environment variables: ADOBE_CLIENT_ID and/or ADOBE_CLIENT_SECRET'); + } + + const scopes = params.ADOBE_TARGET_SCOPES || 'openid,AdobeID,target_sdk,target_admin,target_write,target_read,additional_info.projectedProductContext,read_organizations,additional_info.roles'; + + const response = await fetch('https://ims-na1.adobelogin.com/ims/token/v3', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Cache-Control': 'no-cache' + }, + body: new URLSearchParams({ + client_id: params.ADOBE_CLIENT_ID, + client_secret: params.ADOBE_CLIENT_SECRET, + grant_type: 'client_credentials', + scope: scopes + }) + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error_description || 'Failed to get access token'); + } + + return { + statusCode: 200, + body: { + access_token: data.access_token, + cached: false + } + }; + + } catch (error) { + logger.error('Error getting access token:', error); + return errorResponse(500, error, logger); + } +} + +exports.main = main; +``` + +### Step 5: Export Offers Action + +Create `aio/actions/exportoffers/index.js`: + +```javascript +const { Core } = require('@adobe/aio-sdk'); +const { errorResponse } = require('../utils'); +const targetSDK = require('@adobe/aio-lib-target'); +const openwhisk = require('openwhisk'); +const filesLib = require('@adobe/aio-lib-files'); + +const TARGET_EXPORTS_FILE = 'target-exports.json'; + +async function main(params) { + const logger = Core.Logger('main', { level: params.LOG_LEVEL || 'info' }); + + try { + if (!params.offer || !params.fragmentId || !params.path) { + return errorResponse(400, 'Missing required parameters: offer, fragmentId, and path are required', logger); + } + + const { offer, fragmentId, path } = params; + + // Get access token + const ow = openwhisk(); + const namespace = params.AIO_RUNTIME_NAMESPACE || ''; + const actionName = namespace ? `/${namespace}/sling-da/gettoken` : 'sling-da/gettoken'; + + const tokenResult = await ow.actions.invoke({ + name: actionName, + blocking: true, + result: true + }); + + if (!tokenResult.body || !tokenResult.body.access_token) { + throw new Error('Failed to obtain access token'); + } + + const accessToken = tokenResult.body.access_token; + + // Initialize Target client + const targetClient = await targetSDK.init( + params.ADOBE_TARGET_TENANT, + params.ADOBE_CLIENT_ID, + accessToken + ); + + // Check if offer already exists + const existingOfferId = await getExistingOfferMapping(fragmentId, path, logger); + let isUpdate = false; + let response; + + const offerData = { + name: offer.name, + content: offer.content, + workspace: params.ADOBE_TARGET_WORKSPACE_ID + }; + + const options = { + headers: { + 'Accept': 'application/vnd.adobe.target.v2+json', + } + }; + + if (existingOfferId) { + // Update existing offer + response = await targetClient.updateOffer(existingOfferId, offerData, options); + logger.info(`Successfully updated offer: ${response.body.id}`); + } else { + // Create new offer + response = await targetClient.createOffer(offerData, options); + logger.info(`Successfully created offer: ${response.body.id}`); + } + + // Update target exports mapping + await updateTargetExports(fragmentId, response.body.id, path, logger); + + return { + statusCode: 200, + body: JSON.stringify(response?.body), + headers: { + 'Content-Type': 'application/json' + } + }; + + } catch (error) { + logger.error(`Error processing offer: ${error.message}`); + return errorResponse(500, error, logger); + } +} + +exports.main = main; +``` + +## Deploying and Testing + +### Step 1: Deploy Actions + +```bash +# Navigate to your aio directory +cd aio + +# Deploy the application +aio app deploy +``` + +### Step 2: Set Environment Variables + +```bash +# Set required environment variables +aio app config set ADOBE_CLIENT_ID your_client_id +aio app config set ADOBE_CLIENT_SECRET your_client_secret +aio app config set ADOBE_TARGET_SCOPES "openid,AdobeID,target_sdk,target_admin,target_write,target_read,additional_info.projectedProductContext,read_organizations,additional_info.roles" +aio app config set AIO_RUNTIME_NAMESPACE your_runtime_namespace +aio app config set ADOBE_TARGET_TENANT your_tenant_id +aio app config set ADOBE_TARGET_WORKSPACE_ID your_workspace_id +``` + +### Step 3: Test Actions + +```bash +# Test gettoken action +aio runtime action invoke sling-da/gettoken + +# Test exportoffers action (requires parameters) +aio runtime action invoke sling-da/exportoffers --param '{"offer":{"name":"Test","content":"