UNPKG

@basetime/a2w-api-ts

Version:

Client library that communicates with the addtowallet API.

813 lines (621 loc) 21.1 kB
# AddToWallet Typescript Client Client library that communicates with the addtowallet API. - [Installing](#installing) - [Developing](#developing) - [Building](#building) - [Publishing](#publishing) - [Runtime validation](#runtime-validation) - [Examples](#examples) - [Creating a new client with keys](#creating-a-new-client-with-keys) - [Creating a new client with oauth](#creating-a-new-client-with-oauth) - [Setting a custom user agent](#setting-a-custom-user-agent) - [Custom fetch](#custom-fetch) - [Fetching all campaigns](#fetching-all-campaigns) - [Fetching a pass](#fetching-a-pass) - [Querying for Passes](#querying-for-passes) - [Updating a pass](#updating-a-pass) - [Patching Pass Object Store](#patching-pass-object-store) - [Deleting Object Store Values](#deleting-object-store-values) - [Updating pass logs](#updating-pass-logs) - [Redeem a pass](#redeem-a-pass) - [Get the redeemed status of a pass](#get-the-redeemed-status-of-a-pass) - [Creating a pass bundle](#creating-a-pass-bundle) - [Creating an enrollment](#creating-an-enrollment) - [Fetching all templates](#fetching-all-templates) - [Fetching a template by ID](#fetching-a-template-by-id) - [Fetching templates by tag](#fetching-templates-by-tag) - [Fetching the authenticated organization](#fetching-the-authenticated-organization) - [Get image by ID](#get-image-by-id) - [Get images by IDs](#get-images-by-ids) - [Fetching all scanner apps](#fetching-all-scanner-apps) - [Fetching a scanner app by ID](#fetching-a-scanner-app-by-id) - [Creating a scanner app](#creating-a-scanner-app) - [Updating a scanner app](#updating-a-scanner-app) - [Deleting a scanner app](#deleting-a-scanner-app) - [Updating a campaign](#updating-a-campaign) - [Creating a simple campaign](#creating-a-simple-campaign) - [Cloning a campaign](#cloning-a-campaign) - [Deleting a campaign](#deleting-a-campaign) - [Getting scanner logs for a pass](#getting-scanner-logs-for-a-pass) - [Listing wallets for a campaign](#listing-wallets-for-a-campaign) - [Getting wallet push logs for a pass](#getting-wallet-push-logs-for-a-pass) - [Pushing template updates to wallets](#pushing-template-updates-to-wallets) - [Dismissing pending wallet pushes](#dismissing-pending-wallet-pushes) - [Listing workflows attached to a campaign](#listing-workflows-attached-to-a-campaign) - [Attaching a workflow to a campaign](#attaching-a-workflow-to-a-campaign) - [Updating a campaign workflow](#updating-a-campaign-workflow) - [Detaching a workflow from a campaign](#detaching-a-workflow-from-a-campaign) - [Running a workflow](#running-a-workflow) - [Getting a workflow job status](#getting-a-workflow-job-status) - [Listing and inspecting workflow jobs](#listing-and-inspecting-workflow-jobs) - [Managing webhooks](#managing-webhooks) - [Managing data stores](#managing-data-stores) - [Managing exporters](#managing-exporters) - [Cloning a template](#cloning-a-template) - [Exporting a template](#exporting-a-template) - [Importing a template](#importing-a-template) - [Deleting a template](#deleting-a-template) - [Rendering a barcode](#rendering-a-barcode) - [Signing a JWT with widgets](#signing-a-jwt-with-widgets) ## Installing ```bash npm i @basetime/a2w-api-ts # or yarn add @basetime/a2w-api-ts # or pnpm add @basetime/a2w-api-ts ``` ## Deveoping Run the `watch` command while writing code. ```bash pnpm watch ``` ## Building Run the `build` command to build the code. ```bash pnpm build ``` ## Publishing Commit the code and push to the `main` branch. Then run the `Release` workflow to publish the package to npm. ```bash git commit -m "chore(release): release v0.4.9" git push origin main ``` Then run the `Release` workflow to publish the package to npm. ```bash gh workflow run Release ``` ## Runtime validation Starting with v2.0.0, every type in `src/types/` is defined as a [Zod](https://zod.dev/) schema with an inferred TypeScript type. Both the schema and the type are re-exported from the package root: ```ts import { CampaignSchema, type Campaign } from '@basetime/a2w-api-ts'; ``` At request time, the SDK runs each response through `schema.safeParse(...)`: - On **success** the parsed value is returned. - On **failure** the issue list is logged via the requester's logger as `Response shape mismatch` and the **raw, unvalidated** payload is returned as `T`. This is intentionally non-throwing: a backend response with an unexpected field never crashes a caller, but the mismatch is surfaced loudly to whatever `Logger` was wired in when constructing the `Client` (`console.error` if you passed `console`). Schemas are defensive — `.passthrough()` is used everywhere — so most "new field on the server" cases silently produce a valid parse. If you need stricter validation (throw on shape mismatch), import the schema and call `.parse(...)` yourself: ```ts import { CampaignSchema } from '@basetime/a2w-api-ts'; const campaign = CampaignSchema.parse(await client.campaigns.getById('h8X2JxgrnEsu2U0dI8KN')); ``` Consumers that don't want the runtime validation overhead at all can simply type-import (`import type { Campaign }`), and treat the SDK's response shapes as plain TypeScript types — Zod is bundled into the SDK so there is no extra peer-dependency setup required. ## Examples ### Creating a new client with keys ```ts import { Client, KeysProvider } from '@basetime/a2w-api-ts'; const auth = new KeysProvider('api_key', 'api_secret'); const client = new Client(auth); ``` ### Creating a new client with oauth ```ts import { Client, OAuthProvider } from '@basetime/a2w-api-ts'; const appId = 'a2w-inspector'; const oauth = new OAuthProvider('a2w-inspector'); const client = new Client(oauth); ``` ### Setting a custom user agent ```ts const client = new Client(); client.http.setUserAgent('my-custom-user-agent/1.0.0'); ``` ### Custom fetch All HTTP concerns live on `client.http`, an instance of `HttpRequester` that can also be constructed standalone (for example, in tests). When the client is not pre-configured to use a specific API endpoint, you can use the `fetch` method to make requests to the API, and authentication will be handled automatically. ```ts const t = await client.http.fetch('/templates/simple/l74mNQLcjWnN2AoRRKG0'); console.log(t); ``` ### Fetching all campaigns Fetches the campaigns for the authenticated organization. ```ts const campaigns = await client.campaigns.getAll(); console.log(campaigns); ``` ### Fetching a pass ```ts const campaignId = 'h8X2JxgrnEsu2U0dI8KN'; const passId = '7gXYr76u3Maaf9ugAdWk'; const pass = await client.campaigns.passes.getById(campaignId, passId); console.log(pass); ``` ### Querying for Passes Fetching all of the passes in the campaign. ```ts const campaignId = 'h8X2JxgrnEsu2U0dI8KN'; const passes = await client.campaigns.passes.query(campaignId); console.log(passes); ``` Fetching passes where the primaryKey = '123455'. ```ts const campaignId = 'h8X2JxgrnEsu2U0dI8KN'; const passes = await client.campaigns.passes.query(campaignId, { primaryKey: '123455', }); console.log(passes); ``` Fetching passes where the object store value 'amount' = '30'. ```ts const campaignId = 'h8X2JxgrnEsu2U0dI8KN'; const passes = await client.campaigns.passes.query(campaignId, { 'objectStore.amount': '30', }); console.log(passes); ``` ### Updating a pass Updates the object store. This will also have a2w send the updated pass to the wallets that contain it. Only the following values can be updated: - `templateId` - `templateVersion` - `objectStore` - `passTypeIdentifier` ```ts const campaignId = 'h8X2JxgrnEsu2U0dI8KN'; const passId = '7gXYr76u3Maaf9ugAdWk'; // Each value is optional. const updatedPass = await client.campaigns.passes.update(campaignId, passId, { templateId: '123123123', templateVersion: 2, objectStore: { points: '42', }, }); console.log(updatedPass); ``` ### Patching Pass Object Store Merges the given object store values with the existing object store values. ```ts const campaignId = 'h8X2JxgrnEsu2U0dI8KN'; const passId = '7gXYr76u3Maaf9ugAdWk'; // Each value is optional. const updatedPass = await client.campaigns.passes.mergeObjectStore(campaignId, passId, { objectStore: { points: '42', }, }); console.log(updatedPass); ``` ### Deleting Object Store Values Deletes values from an object store by specifying the keys. ```ts const campaignId = 'h8X2JxgrnEsu2U0dI8KN'; const passId = '7gXYr76u3Maaf9ugAdWk'; // Each value is optional. const updatedPass = await client.campaigns.passes.deleteObjectStoreKeys(campaignId, passId, [ 'points', ]); console.log(updatedPass); ``` ### Updating pass logs Appends a new log to a pass. ```ts const campaignId = 'h8X2JxgrnEsu2U0dI8KN'; const passId = '7gXYr76u3Maaf9ugAdWk'; const ok = await client.campaigns.passes.appendLog(campaignId, passId, 'This is a log message'); console.log(ok); ``` ### Redeem a pass ```ts const campaignId = 'h8X2JxgrnEsu2U0dI8KN'; const passId = '7gXYr76u3Maaf9ugAdWk'; const redeemed = await client.campaigns.passes.redeem(campaignId, passId); console.log(redeemed); ``` ### Get the redeemed status of a pass ```ts const campaignId = 'h8X2JxgrnEsu2U0dI8KN'; const passId = '7gXYr76u3Maaf9ugAdWk'; const redeemed = await client.campaigns.passes.getRedeemedStatus(campaignId, passId); console.log(redeemed); ``` ### Creating a pass bundle ```ts const campaignId = 'h8X2JxgrnEsu2U0dI8KN'; // See the meta value docs for more info. // @see https://avagate.atlassian.net/wiki/spaces/Addto/pages/102891521/Campaigns#Meta-values const meta = { bundle: '9YpI7B8G0dnvRBC1R1a2,9YpI7B8G0dnvRBC1R1a2', banner: 'https://example.com/banner.png', backgroundColor: '#ae00ff', }; // Form values to assign to the pass bundle. Typically, a primary key is set. const form = { primaryKey: '1234567890', }; const url = await client.campaigns.passes.createBundle(campaignId, meta, form); console.log(url); ``` ### Creating an enrollment Creates an enrollment for a campaign, and returns the bundle ID and any errors. ```ts // Meta values to assign to the pass bundle. const meta = { banner: 'https://example.com/banner.png', backgroundColor: '#ae00ff', }; // Form values to assign to the pass bundle. Typically, a primary key is set. const form = { primaryKey: '1234567890', firstName: 'John', lastName: 'Doe', }; // Returns the bundle ID and any errors. const enrollment = await client.campaigns.enrollments.create(campaignId, meta, form); console.log(enrollment.pass, enrollment.errors); ``` ### Fetching all templates Fetches the templates for the authenticated organization. ```ts const templates = await client.templates.getAll(); console.log(templates); ``` ### Fetching a template by ID ```ts const template = await client.templates.getById('id'); console.log(template); ``` ### Fetching templates by tag ```ts const templates = await client.templates.getByTag('tag'); console.log(templates); ``` ### Fetching the authenticated organization ```ts const organization = await client.organizations.getMine(); console.log(organization); ``` ### Get image by ID ```ts const image = await client.images.getById('bWVkrfizHBXyETJEIMk9'); console.log(image); ``` ### Get images by IDs ```ts const images = await client.images.getByIds(['bWVkrfizHBXyETJEIMk9', 'pb6flbrYzrrz4rRSPA7l']); console.log(images); ``` ### Fetching all scanner apps ```ts const apps = await client.scanners.getAll(); console.log(apps); ``` ### Fetching a scanner app by ID ```ts const app = await client.scanners.getById('XVK0xIy2vQinDJWUbKnO'); console.log(app); ``` ### Creating a scanner app `createApp` and `updateApp` accept one of two scanner app input shapes. Standard scanner apps can set the usual scanner fields, but cannot set `jsonConfig` or `jsonConfigUrl`. JSON-configured scanner apps must set `isJsonConfigured: true` and can only set `jsonConfig` and/or `jsonConfigUrl`. ```ts const app = await client.scanners.createApp({ name: 'My Scanner', description: 'My Scanner', tags: ['tag1', 'tag2'], webviewScanUrl: 'https://example.com/scan', webviewStandbyUrl: 'https://example.com/standby', webviewPassword: 'password', passCode: '1234', brandColor: '#ae00ff', brandLogoUrl: 'https://example.com/logo.png', isKioskMode: true, }); console.log(app); ``` For a JSON-configured scanner app: ```ts const app = await client.scanners.createApp({ isJsonConfigured: true, jsonConfigUrl: 'https://example.com/scanner-config.json', }); console.log(app); ``` ### Updating a scanner app ```ts await client.scanners.updateApp('XVK0xIy2vQinDJWUbKnO', { name: 'My Scanner', description: 'My Scanner', tags: ['tag1', 'tag2'], webviewScanUrl: 'https://example.com/scan', webviewStandbyUrl: 'https://example.com/standby', webviewPassword: 'password', passCode: '1234', brandColor: '#ae00ff', brandLogoUrl: 'https://example.com/logo.png', isKioskMode: true, }); ``` For a JSON-configured scanner app update: ```ts await client.scanners.updateApp('XVK0xIy2vQinDJWUbKnO', { isJsonConfigured: true, jsonConfig: '{"theme":"dark"}', }); ``` ### Deleting a scanner app ```ts await client.scanners.deleteApp('XVK0xIy2vQinDJWUbKnO'); ``` ### Updating a campaign Mirrors the backend Joi schema permissively. `templates` accepts a list of template IDs to associate with the campaign. ```ts const updated = await client.campaigns.update('h8X2JxgrnEsu2U0dI8KN', { name: 'Renamed Campaign', templates: ['T01', 'T02'], }); console.log(updated); ``` ### Creating a simple campaign Creates a campaign from an existing template plus placeholder values. Pass `'__new'` as the ID to create a brand-new campaign, or an existing campaign ID to update one in place. ```ts const created = await client.campaigns.createSimple('__new', { campaign: { name: 'My Coupon' }, templateId: 'T01', placeholders: { logo: 'https://example.com/logo.png', backgroundColor: '#ae00ff', }, }); console.log(created); ``` ### Cloning a campaign Returns the ID of the newly created campaign. ```ts const newCampaignId = await client.campaigns.clone('h8X2JxgrnEsu2U0dI8KN'); console.log(newCampaignId); ``` ### Deleting a campaign ```ts await client.campaigns.delete('h8X2JxgrnEsu2U0dI8KN'); ``` ### Getting scanner logs for a pass Returns every scan recorded by a registered scanner against the pass. ```ts const logs = await client.campaigns.passes.getScannerLogs( 'h8X2JxgrnEsu2U0dI8KN', '7gXYr76u3Maaf9ugAdWk', ); console.log(logs); ``` ### Listing wallets for a campaign Returns request logs grouped by bundle, plus the matching bundle entities. Supports optional pagination. ```ts const wallets = await client.campaigns.wallets.getAll('h8X2JxgrnEsu2U0dI8KN', { page: 1, perPage: 50, }); console.log(wallets); ``` You can also fetch a single wallet enrollment: ```ts const enrollment = await client.campaigns.wallets.getEnrollment( 'h8X2JxgrnEsu2U0dI8KN', 'enrollment-id', ); console.log(enrollment); ``` ### Getting wallet push logs for a pass Returns the history of pushes sent to wallets that have the pass installed. ```ts const pushes = await client.campaigns.wallets.getPushLogs( 'h8X2JxgrnEsu2U0dI8KN', '7gXYr76u3Maaf9ugAdWk', ); console.log(pushes); ``` ### Pushing template updates to wallets Pushes the latest template changes to every wallet that contains a pass tied to one of the supplied templates. Returns the number of passes queued for update. ```ts const count = await client.campaigns.wallets.pushTemplates( 'h8X2JxgrnEsu2U0dI8KN', ['T01', 'T02'], ); console.log(count); ``` ### Dismissing pending wallet pushes Clears the "pending changes" notice on a campaign without actually pushing. ```ts await client.campaigns.wallets.dismissPushes('h8X2JxgrnEsu2U0dI8KN'); ``` ### Listing workflows attached to a campaign ```ts const attached = await client.campaigns.workflows.getAll('h8X2JxgrnEsu2U0dI8KN'); console.log(attached); ``` ### Attaching a workflow to a campaign `runsWhen` controls when the workflow fires (`'enrolled'`, `'claimed'`, `'installed'`, `'redeemed'`, `'updated'`, `'scanned'`, or `'scheduled'`). Pass a `schedule` when `runsWhen` is `'scheduled'`. ```ts const attached = await client.campaigns.workflows.attach('h8X2JxgrnEsu2U0dI8KN', { workflowId: 'WF01', runsWhen: 'redeemed', }); console.log(attached); ``` ### Updating a campaign workflow ```ts await client.campaigns.workflows.update('h8X2JxgrnEsu2U0dI8KN', 'CWF01', { runsWhen: 'scheduled', schedule: { when: 'daily', weekday: '', monthday: '', time: '09:00' }, }); ``` ### Detaching a workflow from a campaign Returns the remaining workflow attachments. ```ts const remaining = await client.campaigns.workflows.detach( 'h8X2JxgrnEsu2U0dI8KN', 'CWF01', ); console.log(remaining); ``` ### Running a workflow Creates a new workflow job and dispatches it to the runner. Returns the job in the `pending` status; poll `client.workflows.jobs.getStatus(jobId)` to track progress. ```ts const job = await client.workflows.run({ workflowId: 'WF01', campaign: 'h8X2JxgrnEsu2U0dI8KN', pass: '7gXYr76u3Maaf9ugAdWk', }); console.log(job.id); ``` ### Getting a workflow job status ```ts const status = await client.workflows.jobs.getStatus('JOB01'); console.log(status); // 'pending' | 'running' | 'success' | 'error' ``` ### Listing and inspecting workflow jobs ```ts const allJobs = await client.workflows.jobs.getAll('WF01'); const job = await client.workflows.jobs.getById('JOB01'); await client.workflows.jobs.update('JOB01', { status: 'success' }); await client.workflows.jobs.addLog('JOB01', { type: 'info', message: 'Completed' }); ``` ### Managing webhooks ```ts const webhooks = await client.organizations.webhooks.getAll(); const created = await client.organizations.webhooks.create({ displayName: 'Redemption Notifier', url: 'https://example.com/hooks/redemption', event: 'redeemed', password: 'shared-secret', }); await client.organizations.webhooks.update(created.id, { ...created, displayName: 'Renamed', }); await client.organizations.webhooks.delete(created.id); const logs = await client.organizations.webhooks.getLogs(); console.log(logs); ``` ### Managing data stores ```ts const stores = await client.organizations.dataStores.getAll(); const created = await client.organizations.dataStores.create({ name: 'Member tiers', source: 'key-value', keyValue: [ { key: 'gold', value: '1000' }, { key: 'silver', value: '500' }, ], }); const fetched = await client.organizations.dataStores.getById(created.id); await client.organizations.dataStores.update(created.id, { ...created, name: 'Renamed', }); await client.organizations.dataStores.delete(created.id); ``` ### Managing exporters ```ts const exporters = await client.organizations.exporters.getAll(); const created = await client.organizations.exporters.create({ name: 'Nightly SFTP', what: 'enrollments', when: 'daily', time: '03:00', source: 'sftp', config: { hostname: 'sftp.example.com', username: 'a2w', password: 'secret', filename: 'enrollments.csv', }, }); const ex = created[0]; await client.organizations.exporters.run(ex.id); const logs = await client.organizations.exporters.getLogs(ex.id); console.log(logs); await client.organizations.exporters.delete(ex.id); ``` ### Cloning a template ```ts const cloned = await client.templates.clone('TPL01'); console.log(cloned); ``` ### Exporting a template Returns the JSON bundle that can be re-imported into another organization. ```ts const bundle = await client.templates.export('TPL01'); console.log(bundle); ``` ### Importing a template Pass a `Blob`/`File` or a `{ name, content }` shape and the SDK constructs the multipart upload for you. ```ts const imported = await client.templates.import({ name: 'tpl.json', content: JSON.stringify(bundle), }); console.log(imported); ``` ### Deleting a template ```ts await client.templates.delete('TPL01'); ``` ### Rendering a barcode The barcode endpoint lives at the site root (outside `/api/v1`). The PNG body is returned as a string. ```ts const png = await client.barcodes.render({ type: 'qrcode', data: 'hello', width: 300, height: 300, }); ``` ### Signing a JWT with widgets `signJwt` signs an arbitrary payload with an explicit secret. `signCampaignJwt` signs using a campaign's stored `openEnrollmentJwtSecret`, which is what `client.campaigns.enrollments.create` needs. Wiring it in: ```ts const campaignId = 'h8X2JxgrnEsu2U0dI8KN'; client.campaigns.enrollments.jwtEncode = (data) => client.widgets.signCampaignJwt(campaignId, data); const enrollment = await client.campaigns.enrollments.create( campaignId, { backgroundColor: '#ae00ff' }, { primaryKey: '1234567890', firstName: 'John', lastName: 'Doe' }, ); console.log(enrollment.pass, enrollment.errors); ``` Or sign an arbitrary payload directly: ```ts const token = await client.widgets.signJwt({ sub: 'user-1' }, 'shared-secret'); ```