# Salesforce Commerce Cloud (SFCC)

This integration explains how to add a **unique “Add to Wallet” button** to a Salesforce Commerce Cloud storefront. Common placements are the **My Account** area (loyalty or membership pass) and authenticated pages where a stable identifier is available (for example a gift card list). Pick & collect pages can also be a fit when a pickup pass is used as an in-store scan token.

<figure><img src="https://3566051324-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FWLc8AHXW4tdrAXUBfrYF%2Fuploads%2FMAXVV8T3j5sxIpwRScLd%2Fimage.png?alt=media&#x26;token=5e5cbb0d-f7c9-4bdf-8aa1-29dd5e6fe809" alt="Salesforce Commerce Cloud and Apple/Google Wallet integration with add to wallet button"><figcaption></figcaption></figure>

A stable identifier available in the customer session is signed server-side, then passed to The Wallet Crew cinto SDK as an external identifier. This keeps the identifier exchange tamper-proof and avoids exposing secrets in storefront code.

<details>

<summary><strong>Real-world examples</strong></summary>

* A loyalty program renders “Add to Wallet” in **My Account** for logged-in customers.
* A gift card program renders one button per **active** gift card in the account wallet.
* A pick & collect flow renders a pickup pass CTA on a dedicated pickup page, using a pickup reference as identifier.

</details>

{% hint style="info" %}
For generic cinto SDK usage (pass id mode, platform detection, desktop QR customization), see [On your website](https://docs.thewalletcrew.io/enroll/on-your-website).
{% endhint %}

## SFCC concepts used in this integration

Salesforce Commerce Cloud storefront code is packaged and deployed as **cartridges**. A cartridge can add controllers, scripts, templates, and configuration metadata, then be plugged into a site by adding it to the **cartridge path**.

This guide assumes one dedicated cartridge is created for The Wallet Crew integration, typically named like `int_TheWalletCrew`. The cartridge is then referenced by:

* the storefront application (SFRA or SiteGenesis)
* Business Manager configuration (site preferences and, optionally, custom objects)

### Cartridge

A **cartridge** is the unit of deployment in SFCC. Cartridge order matters because SFCC resolves templates and scripts by scanning the cartridge path from left to right.

### SFRA vs SiteGenesis

Both architectures can support the integration:

* **SFRA**: controllers and ISML templates. The button is usually added in an account template or a remote include.
* **SiteGenesis**: pipelines (legacy) and ISML templates. The button is usually added in a pipeline-rendered account page.

The security model stays identical. The HMAC is computed server-side and never in browser JavaScript.

## How it works

The Wallet Crew cinto SDK renders the correct CTA based on device.

On iOS, the button downloads an Apple Wallet pass. On Android, it opens Google Wallet. On desktop, the SDK redirects to a hosted pass page that can be scanned or opened on mobile.

In this SFCC setup, the pass is retrieved using:

* a **pass type** (example: `user`)
* a stable SFCC identifier sent as an **external identifier** (example: `sfcc.customerNo`)
* an **HMAC** signature that proves the identifier was issued by the Brand backend

{% hint style="warning" %}
The HMAC must be computed server-side. It must not be generated in the browser.
{% endhint %}

## Prerequisites

* A pass template and pass issuance already configured in The Wallet Crew.
* The Wallet Crew tenant id available (used by the SDK script URL).
* A stable identifier available on authenticated pages, such as:
  * SFCC `CustomerNo` (typical for loyalty cards)
  * pickup reference (typical for pick & collect pickup passes)
  * gift card id (when gift cards are stored in SFCC or a connected system)
* The external identifier key name agreed during onboarding (example: `sfcc.customerNo`).

## Implement the integration as a cartridge

The typical pattern is:

1. Read the identifier in SFCC server-side code.
2. Compute an HMAC with the tenant secret.
3. Render a cinto container in the ISML template with identifier + HMAC.
4. Load the cinto SDK from `sdk.neostore.cloud`.

{% stepper %}
{% step %}

#### Add the cartridge to the cartridge path

After deploying the integration cartridge to the instance, add it to the site’s cartridge path in Business Manager. The cartridge should be placed **before** the base storefront cartridge so overrides resolve correctly.

{% hint style="info" %}
Exact Business Manager menu labels vary by SFCC version. The key point is the **site-level cartridge path** (not the global one).
{% endhint %}
{% endstep %}

{% step %}

#### Store tenant configuration as site preferences

Tenant values and secrets should remain server-side. In SFCC, this is typically done with **custom site preferences**.

At minimum, these values are usually required.

<details>

<summary><strong>Recommended preferences (IDs, types, examples)</strong></summary>

These values are typically created as **site-level custom preferences** (not organization-level).

* `theWalletCrewTenantId` (String)\
  Example: `molia`
* `theWalletCrewHmacSecret` (Password)\
  Example: `I1M8emrrJSns4Hnuibbm45eWfLQMosPGKSp1JzKsCrXeWmhjE8lZhxC2tfSRX5IJ`
* `theWalletCrewDefaultPassType` (String)\
  Example: `user`
* `theWalletCrewExternalIdentifierKey` (String)\
  Example: `sfcc.customerNo`
* `theWalletCrewLanguage` (String, optional)\
  Example: `fr`\
  When omitted, cinto uses browser language detection.

{% hint style="info" %}
Keeping the secret as a **Password** preference helps prevent accidental disclosure in UI exports and screenshots.
{% endhint %}

</details>

{% hint style="warning" %}
The HMAC secret must not be stored in templates, content assets, or any client-side configuration.
{% endhint %}
{% endstep %}

{% step %}

#### Compute the HMAC server-side

SFCC server-side scripts can compute a SHA-256 HMAC using the tenant secret and the exact identifier value.

Common implementation choices:

* a helper script exposed by the cartridge, used by account controllers
* a decorator pattern that enriches the view model with `identifier` and `identifierHmac`

Caching must be handled carefully. Customer-specific pages should not share cached HTML across customers when the identifier is embedded in the markup.
{% endstep %}

{% step %}

#### Render the “Add to Wallet” button in ISML

The cinto SDK relies on:

* one SDK loader script referencing the tenant id
* one container element marked as an “Add to Wallet” button
* a `passType`
* an external identifier key/value pair plus the server-side HMAC

The integration cartridge typically provides an ISML partial that can be included where needed (account dashboard, gift card list, pick & collect page).
{% endstep %}
{% endstepper %}

## External identifier conventions (casing + escaping)

The cinto SDK can retrieve a pass from an external identifier:

* `passType` (example: `user`)
* `externalIdentifier.key` (example: `sfcc.customerNo`)
* `externalIdentifier.value` (example: `00012345`)
* `externalIdentifier.hmac` (HMAC-SHA256 of the value)

### Recommended key naming convention

The most stable option is to avoid mixed case in identifier keys.

Keys are typically:

* namespaced with a prefix like `sfcc.`
* lowercase
* using `_` as separator when needed

Examples:

* `sfcc.customer_no`
* `sfcc.pickup_ref`
* `sfcc.gift_card_id`

{% hint style="info" %}
If The Wallet Crew tenant is already configured with a mixed-case key, changing it can impact existing installations. A key naming change should be treated as a migration topic.
{% endhint %}

### If a mixed-case key must be used in HTML attributes

Browsers treat HTML attribute names as lower case. cinto supports an escaping rule for uppercase characters in `data-neostore-externalIdentifiers-...` attributes:

* Prefix an uppercase character with `_` in the attribute name.
* `y2.customer_Id` is interpreted as `y2.customerId`.

Applied to SFCC examples:

* Identifier key in The Wallet Crew: `sfcc.customerNo`
* Identifier key in HTML attributes: `sfcc.customer_No`

This only affects the **attribute name**, not the signed value.

## Copy/paste SFRA reference implementation (controller + ISML partial)

This reference implementation uses:

* **site preferences** to store tenant configuration
* a **server-side HMAC helper**
* a **remote include controller** that renders an ISML partial

It is designed to be pasted into a dedicated cartridge (example: `int_TheWalletCrew`) and called from an account page.

### Cartridge file layout

* `cartridge/scripts/TheWalletCrew/hmac.js`
* `cartridge/controllers/TheWalletCrew.js`
* `cartridge/templates/default/TheWalletCrew/addToWalletButton.isml`

### 1) HMAC helper (`cartridge/scripts/TheWalletCrew/hmac.js`)

{% code title="cartridge/scripts/TheWalletCrew/hmac.js" %}

```javascript
'use strict';

var Mac = require('dw/crypto/Mac');
var Encoding = require('dw/crypto/Encoding');

/**
 * HMAC-SHA256 over the raw identifier string (UTF-8), encoded as hex.
 * Output is lowercased to match cinto examples.
 */
function hmacSha256Hex(secret, identifierValue) {
    var mac = new Mac(Mac.HMAC_SHA_256);
    var bytes = mac.digest(identifierValue, secret);
    return Encoding.toHex(bytes).toLowerCase();
}

module.exports = {
    hmacSha256Hex: hmacSha256Hex
};
```

{% endcode %}

### 2) Controller (`cartridge/controllers/TheWalletCrew.js`)

This controller renders a widget that is safe to include as a remote include.

{% code title="cartridge/controllers/TheWalletCrew\.js" %}

```javascript
'use strict';

var server = require('server');
var Site = require('dw/system/Site');

var theWalletCrewHmac = require('*/cartridge/scripts/TheWalletCrew/hmac');

server.get('Button', function (req, res, next) {
    // Disable caching to avoid serving one customer’s identifier to another.
    // Cache-control strategy differs across SFRA baselines, so keep it explicit.
    res.setHttpHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
    res.setHttpHeader('Pragma', 'no-cache');

    var site = Site.getCurrent();
    var tenantId = site.getCustomPreferenceValue('theWalletCrewTenantId');
    var secret = site.getCustomPreferenceValue('theWalletCrewHmacSecret');
    var passType = site.getCustomPreferenceValue('theWalletCrewDefaultPassType') || 'user';
    var externalKey = site.getCustomPreferenceValue('theWalletCrewExternalIdentifierKey') || 'sfcc.customerNo';
    var language = site.getCustomPreferenceValue('theWalletCrewLanguage');

    // Example identifier: logged-in customer number.
    var identifierValue = (customer && customer.profile) ? customer.profile.customerNo : null;

    if (!tenantId || !secret || !identifierValue) {
        res.render('TheWalletCrew/addToWalletButton', { enabled: false });
        return next();
    }

    var identifierHmac = theWalletCrewHmac.hmacSha256Hex(secret, identifierValue);

    // Recommended in server-side templates: use the JSON attribute form.
    // It avoids HTML attribute casing pitfalls.
    var externalIdentifiers = {};
    externalIdentifiers[externalKey] = { value: identifierValue, hmac: identifierHmac };

    res.render('TheWalletCrew/addToWalletButton', {
        enabled: true,
        tenantId: tenantId,
        passType: passType,
        language: language,
        externalIdentifiersJson: JSON.stringify(externalIdentifiers)
    });

    return next();
});

module.exports = server.exports();
```

{% endcode %}

### 3) ISML partial (`cartridge/templates/default/TheWalletCrew/addToWalletButton.isml`)

{% code title="cartridge/templates/default/TheWalletCrew/addToWalletButton.isml" %}

```html
<isif condition="${pdict.enabled}">
    <script type="text/javascript">
    (function (n, e, o) {
        var s=n.createElement("script");
        s.src="https://sdk.neostore.cloud/scripts/"+e+"/cinto@1";
        s.async=1;
        s.onload=function(){neostore.cinto.initialize(e,o);};
        n.body.appendChild(s);
    })(document, "${pdict.tenantId}", <isprint value="${pdict.language ? ('{ \"language\": \"' + pdict.language + '\" }') : '{ }'}" encoding="off" />);
    </script>

    <div
        data-neostore-addToWalletButton
        data-neostore-passType="${pdict.passType}"
        data-neostore-externalIdentifiers="<isprint value="${pdict.externalIdentifiersJson}" encoding="off" />">
    </div>
</isif>
```

{% endcode %}

{% hint style="warning" %}
The script loader must be included **once per page**. When several buttons are rendered on the same page, move the loader into a shared footer partial and keep only the `<div data-neostore-addToWalletButton ...>` markup in the widget template.
{% endhint %}

### 4) Include the widget in an account template

In SFRA, remote includes are commonly used to avoid polluting the main controller and to keep caching explicit.

{% code title="Example include (ISML)" %}

```html
<isinclude url="${URLUtils.url('TheWalletCrew-Button')}" />
```

{% endcode %}

## Content Security Policy (CSP)

Some SFCC storefronts enforce a Content Security Policy that blocks third-party scripts by default. If the SDK script is blocked, allowlisting `https://sdk.neostore.cloud` for `script-src` is required.

In SFRA projects, CSP is often managed via middleware and response headers. The change should follow the project’s existing CSP strategy (report-only vs enforced).

## Validate the result

Validation typically covers platform rendering and pass installation.

1. On iOS Safari, the CTA should display **Add to Apple Wallet**.
2. On Android Chrome, the CTA should display **Add to Google Wallet**.
3. On desktop, the CTA should redirect to a hosted pass page.
4. After installation, the pass should be visible in Apple Wallet or Google Wallet.

## Troubleshooting

### The button does not render

Common causes are a blocked SDK script (CSP) or missing markup. The rendered page should contain the SDK loader and the cinto container element.

### Clicking the button leads to an error

This usually means the external identifier or HMAC does not match what The Wallet Crew expects. Confirm:

* the external identifier key name matches the one configured for SFCC (example: `sfcc.customerNo`)
* the identifier value matches exactly what is signed server-side (no trimming, formatting, or type changes)
* the HMAC was computed with the correct tenant secret

### The button renders for the wrong customer

This typically indicates HTML caching across sessions. Ensure that:

* customer-specific pages are not cached at page level
* remote includes used for account widgets are not cached across customers
* any CDN layer respects session cookies for authenticated pages

## FAQ

<details>

<summary><strong>What is a cartridge in SFCC?</strong></summary>

A cartridge is the deployable unit that contains storefront code and metadata. The site’s cartridge path defines which cartridges are active and in which order they are resolved.

</details>

<details>

<summary><strong>Which identifier works best for loyalty cards?</strong></summary>

The SFCC customer number (`CustomerNo`) is commonly used because it is stable and available on authenticated pages. A CRM id can also be used if it is persisted on the profile and stays stable over time.

</details>

<details>

<summary><strong>Where should the tenant secret be stored in SFCC?</strong></summary>

Custom site preferences are typically used because they stay server-side and can be managed per site. The secret should never be exposed in templates, content assets, or browser JavaScript.

</details>

<details>

<summary><strong>Can the same approach be used for gift cards and pick &#x26; collect?</strong></summary>

Yes. The same pattern works as long as a stable identifier exists for the object and the button can be rendered on a page that has access to that identifier server-side.

</details>

<details>

<summary><strong>Does the integration require SFRA?</strong></summary>

No. SFRA and SiteGenesis can both support the same server-side HMAC + template rendering pattern. Only the implementation entry points differ (controllers vs pipelines).

</details>
