# Pass installation and uninstallation hooks

It is possible to create a **custom connector** in **The Wallet Crew** to call an external API whenever a pass is **installed** or **uninstalled** on Apple Wallet or Google Wallet.

This allows Brands to **synchronize installation status** with external systems such as a CRM, analytics tool, or marketing automation platform.

<details>

<summary>Real-world examples</summary>

* A retail Brand updates a CRM field like `hasWalletPass=true` right after installation. It then stops sending “add to wallet” reminders.
* A ticketing Brand logs installs and uninstalls in its BI stack. It monitors adoption by event, channel, and device type.
* A Brand triggers a welcome journey after installation. It uses `registrationSource` to segment by UTM campaign.
* A Brand detects unusual uninstall spikes after a bad release. It rolls back quickly and communicates proactively.

</details>

### Runtime Hooks

The platform exposes the following hooks on the `runtime.wallet.passUpdater` endpoint:

* **`OnPassInstalled`** → Triggered when a pass is added to Apple/Google Wallet
* **`OnPassUninstalled`** → Triggered when a pass is removed from Apple/Google Wallet

Both methods receive the same parameters:

| Parameter               | Type                                    | Description                                                   |
| ----------------------- | --------------------------------------- | ------------------------------------------------------------- |
| `passId`                | `string`                                | Unique identifier of the pass                                 |
| `passType`              | `string`                                | Type of pass (loyalty, gift card, event ticket…)              |
| `identifiers`           | `Record<string, any>`                   | Key/value identifiers defined on the pass (e.g. `customerId`) |
| `device`                | `"apple"` or `"google"`                 | Wallet platform                                               |
| `additionalInformation` | `AdditionalPassInstallationInformation` | Includes registration statistics                              |

### Example Implementation

Scripts can be placed in the `server/scripts` folder under advanced configuration.

```js
const API_URL = 'https://partner';
import { getSecret } from 'neo/secrets';

/** 
 * @typedef {Object} RegistrationInformation 
 * @property {number} activeRegistrationCount - Active registrations. 
 * @property {number} totalRegistrationCount - Total registrations. 
 **/ 

/**
 * @typedef {Object} RegistrationSource
 * @property {string[]} tags - list of tags specified by the SDK integration
 * @property {string} medium - medium specified by the SDK integration or utm_medium
 * @property {string} origin  - url from where the pass is installed - could be overriden by the SDK integration
 * @property {string} userAgent - optional UserAgent used to install the pass
 */

/** 
 * @typedef {Object} AdditionalPassInstallationInformation
 * @property {RegistrationInformation} registrationInformation - Registration statistics. 
 * @property {RegistrationSource} registrationSource - Source of the installation when available 
 */

/** methods triggered when a pass is installed 
 * @param {string} passId - unique identifier of the pass
 * @param {string} passType - pass type of the current pass
 * @param {Record<string, any>} identifiers - key value pairs of identifiers of the pass
 * @param {"apple"|"google"} device - named of the device
 * @param {AdditionalPassInstallationInformation} additionalInformation - additionalInformation related to this pass installation */
 async function onPassInstalled(passId, passType, identifiers, device, additionalInformation) {
     await onPassInstallationStatusChanged(passId, passType, identifiers, device, true);
} 

/** methods triggered when a pass is uninstalled 
 * @param {string} passId - unique identifier of the pass 
 * @param {string} passType - pass type of the current pass
 * @param {Record<string, any>} identifiers - key value pairs of identifiers of the pass
 * @param {"apple"|"google"} device - named of the device 
 * @param {AdditionalPassInstallationInformation} additionalInformation - additionalInformation related to this pass installation 
 */ 
async function onPassUninstalled(passId, passType, identifiers, device, additionalInformation) { 
    await onPassInstallationStatusChanged(passId, passType, identifiers, device, false); 
} 

async function onPassInstallationStatusChanged(passId, passType, identifiers, device, isInstalled) {
  const customerId = identifiers["customerId"]; // or any other external identifier
  const API_SECRET = await getSecret('PARTNER-API-SECRET');

  // ⚠️ Note: fetch is not standard here. It uses PascalCase options and object-based Body.
  await fetch(API_URL + '/wallet/installation', {
    Method: "POST",
    Headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      "X-API-KEY": API_SECRET
    },
    Body: {
      customerId,
      isInstalled,
      device
    },
    ThrowOnError: false
  });
}

export default function (context) {
  context.register('runtime.wallet.passUpdater', {
    OnPassInstalled: onPassInstalled,
    OnPassUninstalled: onPassUninstalled
  });
}
```

### Minimal Example

For testing or logging only:

```js
async function onPassInstalled(passId, passType, identifiers, device) {
  console.log(`Pass ${passId} installed on ${device}`);
}

async function onPassUninstalled(passId, passType, identifiers, device) {
  console.log(`Pass ${passId} uninstalled from ${device}`);
}

export default function (context) {
  context.register('runtime.wallet.passUpdater', {
    OnPassInstalled: onPassInstalled,
    OnPassUninstalled: onPassUninstalled
  });
}
```

### When Are Events Triggered?

* **`OnPassInstalled`** → fired when a user successfully adds a pass to Apple or Google Wallet.
* **`OnPassUninstalled`** → fired when a user removes the pass from their wallet.
* Events are reliable: the platform ensures correct delivery even under high load.

### Notes

* `fetch` is **not the standard browser fetch** — it accepts PascalCase options (`Method`, `Headers`, `Body`, `ThrowOnError`).
* Authentication is flexible: you may use API keys (via `getSecret`) or OAuth with the custom `fetch`.
* There are no platform restrictions: execution is safe and fully managed.

### FAQ

<details>

<summary>Are these events real-time?</summary>

They trigger when the pass is added or removed in the wallet. In practice, you should treat them as near real-time. Always design your API to be idempotent, so retries do not create duplicates.

</details>

<details>

<summary>What happens if my API is down?</summary>

Your connector code controls the call. You should handle timeouts and errors gracefully. If the external system is critical, implement retries and a dead-letter strategy on your side.

</details>

<details>

<summary>Can I use this to trigger emails or push campaigns?</summary>

Yes. The most common pattern is to call your marketing automation platform (directly or via your backend) and trigger a journey on install. Use uninstall events to stop or adapt communications.

</details>

<details>

<summary>What is included in <code>additionalInformation</code>?</summary>

It can include registration statistics and, when available, the installation source. Use it to measure adoption and attribute installs to a campaign or SDK integration.

</details>
