# Adobe Commerce (Magento)

This integration explains how to add a **unique “Add to Wallet” button** to the **My Account** area in **Adobe Commerce (Magento 2)**. This Magento Apple Wallet / Google Wallet integration helps customers save a **loyalty or membership pass** (and optionally a **wallet gift card**) directly in their mobile wallet, using an identifier available in the Adobe Commerce customer session.

<figure><img src="https://3566051324-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FWLc8AHXW4tdrAXUBfrYF%2Fuploads%2FCY6Kgh7mmUXLfmUr2Twl%2Fimage.png?alt=media&#x26;token=ecec6631-c67d-40f6-9a62-a84b2f4a9496" alt="Adobe Commerce My Account page showing an “Add to Wallet” button."><figcaption></figcaption></figure>

<details>

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

* A loyalty program shows “Add to Wallet” next to the membership number in **My Account**.
* A gift card program shows an “Add to Wallet” button only for **active** gift cards.
* A pick & collect flow renders a pickup pass CTA on a dedicated pickup page, where a pickup reference is available server-side.

</details>

Adobe Commerce and Magento Open Source share the same Magento 2 storefront architecture. The implementation patterns in this page apply to both, with minor differences in deployment and security configuration.

{% 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 %}

### 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 Adobe Commerce setup, the pass is retrieved using:

* A **pass type** (example: `user`)
* A Magento customer identifier sent as an **external identifier** (example: `magento.customer_Id`)
* 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 customer identifier available in the session on the account page. This identifier is used by The Wallet Crew to retrieve or create the pass. The identifier key name (example: `magento.customer_Id`) is defined during project onboarding.

### Add the button to the Adobe Commerce customer account page

Adobe Commerce storefront pages are built from **layout XML** and **PHTML templates**. Rendering the button from a Magento block keeps secrets server-side and enables safe escaping for HTML attributes.

The reference implementation below adds the button to the default customer dashboard page (`customer/account`).

#### Store the secret used to compute HMAC

The HMAC secret must stay server-side. Two common options are used in Adobe Commerce projects.

**Option A (recommended): store values in `app/etc/env.php`**

Store values in deployment config so they are not editable from the Adobe Commerce Admin.

{% code title="app/etc/env.php (example)" %}

```php
<?php
return [
    // ...
    'walletcrew' => [
        'tenant_id' => '<<tenantId>>',
        'hmac_secret' => '<<hmacSecret>>',
    ],
];
```

{% endcode %}

**Option B: store the secret as an environment variable**

This keeps secrets out of the codebase and out of the database. It also works well with container deployments.

{% code title="Environment variables (example)" %}

```
THEWALLETCREW_TENANT_ID=<<tenantId>>
THEWALLETCREW_HMAC_SECRET=<<hmacSecret>>
```

{% endcode %}

{% hint style="info" %}
In **Adobe Commerce Cloud**, environment variables are often used as the source of truth for secrets. The `app/etc/env.php` file is commonly generated during deployment.
{% endhint %}

{% hint style="warning" %}
Storing the secret in theme files or rendering it in HTML must be avoided. Only the computed HMAC should reach the storefront.
{% endhint %}

#### Reference implementation (layout XML + block + PHTML)

This implementation creates:

* a **Block** class that reads the logged-in customer id and computes the HMAC
* a **PHTML template** that renders the SDK loader + `<div data-neostore-addToWalletButton ...>`
* a **layout XML** entry that inserts the block in the customer account dashboard

It also requires a minimal module scaffold so Adobe Commerce can load the files.

{% code title="app/code/Vendor/WalletCrewCinto/registration.php" %}

```php
<?php
use Magento\Framework\Component\ComponentRegistrar;

ComponentRegistrar::register(
    ComponentRegistrar::MODULE,
    'Vendor_WalletCrewCinto',
    __DIR__
);
```

{% endcode %}

{% code title="app/code/Vendor/WalletCrewCinto/etc/module.xml" %}

```xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Vendor_WalletCrewCinto" setup_version="1.0.0"/>
</config>
```

{% endcode %}

After the files are deployed, enable the module and refresh generated files and caches.

{% code title="Enable the module (example)" %}

```bash
bin/magento module:enable Vendor_WalletCrewCinto
bin/magento setup:upgrade
bin/magento cache:flush
```

{% endcode %}

{% stepper %}
{% step %}

#### Add the layout XML on the customer dashboard

Add a layout update to inject a block into the dashboard content container.

{% code title="app/code/Vendor/WalletCrewCinto/view/frontend/layout/customer\_account\_index.xml" %}

```xml
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="content">
            <block class="Vendor\WalletCrewCinto\Block\AddToWallet"
                   name="walletcrew.add_to_wallet"
                   template="Vendor_WalletCrewCinto::add-to-wallet.phtml"
                   cacheable="false"/>
        </referenceContainer>
    </body>
</page>
```

{% endcode %}

`cacheable="false"` ensures customer-specific identifiers are not cached across sessions.
{% endstep %}

{% step %}

#### Implement the Block (compute HMAC server-side)

The block reads the logged-in customer id and computes `hash_hmac('sha256', $value, $secret)`.

{% code title="app/code/Vendor/WalletCrewCinto/Block/AddToWallet.php" %}

```php
<?php
declare(strict_types=1);

namespace Vendor\WalletCrewCinto\Block;

use Magento\Customer\Model\Session as CustomerSession;
use Magento\Framework\App\DeploymentConfig;
use Magento\Framework\Escaper;
use Magento\Framework\View\Element\Template;

final class AddToWallet extends Template
{
    public function __construct(
        Template\Context $context,
        private readonly CustomerSession $customerSession,
        private readonly DeploymentConfig $deploymentConfig,
        private readonly Escaper $escaper,
        array $data = []
    ) {
        parent::__construct($context, $data);
    }

    public function getTenantId(): string
    {
        $fromEnvPhp = (string)($this->deploymentConfig->get('walletcrew/tenant_id') ?? '');
        if ($fromEnvPhp !== '') {
            return $fromEnvPhp;
        }

        return (string)(getenv('THEWALLETCREW_TENANT_ID') ?: getenv('WALLETCREW_TENANT_ID'));
    }

    public function getCustomerId(): ?string
    {
        $id = $this->customerSession->getCustomerId();
        return $id ? (string)$id : null;
    }

    public function getCustomerIdHmac(): ?string
    {
        $customerId = $this->getCustomerId();
        if ($customerId === null) {
            return null;
        }

        $secret = (string)($this->deploymentConfig->get('walletcrew/hmac_secret') ?? '');
        if ($secret === '') {
            $secret = (string)(getenv('THEWALLETCREW_HMAC_SECRET') ?: getenv('WALLETCREW_HMAC_SECRET'));
        }
        if ($secret === '') {
            return null;
        }

        return hash_hmac('sha256', $customerId, $secret);
    }

    public function escAttr(?string $value): string
    {
        return $this->escaper->escapeHtmlAttr((string)$value);
    }
}
```

{% endcode %}

{% hint style="info" %}
This example uses the Magento customer id as `magento.customer_Id`. If a CRM id is stored on the customer entity, it can be used instead.
{% endhint %}
{% endstep %}

{% step %}

#### Render the cinto button in a PHTML template

The template injects tenant id, customer id, and HMAC from the block.

{% code title="app/code/Vendor/WalletCrewCinto/view/frontend/templates/add-to-wallet.phtml" %}

```php
<?php
/** @var \Vendor\WalletCrewCinto\Block\AddToWallet $block */
$tenantId = $block->getTenantId();
$customerId = $block->getCustomerId();
$hmac = $block->getCustomerIdHmac();

if (!$tenantId || !$customerId || !$hmac) {
    return;
}
?>

<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, "<?= $block->escAttr($tenantId) ?>", { language: "fr" });
</script>

<div data-neostore-addToWalletButton
     data-neostore-passType="user"
     data-neostore-externalIdentifiers-magento.customer_Id-value="<?= $block->escAttr($customerId) ?>"
     data-neostore-externalIdentifiers-magento.customer_Id-hmac="<?= $block->escAttr($hmac) ?>"
></div>
```

{% endcode %}

If a different language is required, update `{ language: "fr" }`. If omitted, the SDK uses the browser language.

{% hint style="info" %}
The full cinto SDK documentation (options, desktop fallback, and platform detection) is available in [On your website](https://docs.thewalletcrew.io/enroll/on-your-website).
{% endhint %}
{% endstep %}
{% endstepper %}

#### Notes about the external identifier name

Browsers lowercase HTML attributes. The cinto SDK includes an escape rule to preserve uppercase characters by prefixing them with `_`.

This is why `magento.customer_Id` is written with an underscore before `I`. Without the underscore, `customer_Id` would be interpreted as `customerid`.

#### Optional: Adobe Commerce CSP allowlist

Some Adobe Commerce setups enforce a Content Security Policy that can block third-party scripts by default. If the SDK script is blocked, allowlist `https://sdk.neostore.cloud`.

{% hint style="info" %}
Magento CSP configuration differs across versions and setups. If CSP is enabled, alignment with the project’s existing CSP strategy is required.
{% endhint %}

If the project uses Magento’s CSP whitelist mechanism, allowlisting the SDK host for `script-src` is usually enough.

{% code title="app/code/Vendor/WalletCrewCinto/etc/csp\_whitelist.xml (example)" %}

```xml
<?xml version="1.0"?>
<csp_whitelist xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Csp:etc/csp_whitelist.xsd">
    <policies>
        <policy id="script-src">
            <values>
                <value id="walletcrew-sdk" type="host">https://sdk.neostore.cloud</value>
            </values>
        </policy>
    </policies>
</csp_whitelist>
```

{% endcode %}

{% hint style="warning" %}
Adobe Commerce projects often centralize CSP management (report-only vs enforced). The allowlist location and process should match project conventions.
{% endhint %}

### 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 final HTML should contain the cinto SDK loader and the `<div data-neostore-addToWalletButton ...>` 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 Magento (example: `magento.customer_Id`).
* The HMAC was computed with the correct tenant secret and the exact identifier value.

**The wrong language is displayed**

Set `{ language: "fr" }` (or another ISO 639 language code) in the `initialize()` options.

### FAQ

<details>

<summary><strong>Does this require a custom module?</strong></summary>

Not strictly. A theme override can be enough when server-side values for the customer identifier and HMAC can be rendered. A custom module is usually preferred because it keeps HMAC computation out of templates and centralizes secret loading (deployment config or environment variables).

</details>

<details>

<summary><strong>Which customer identifier should be used?</strong></summary>

Adobe Commerce’s internal customer id is commonly used because it is stable and always available on the account page. A project can also use a CRM id if it is stored on the customer entity. The identifier must stay stable over time.

</details>

<details>

<summary><strong>Where does the HMAC secret come from?</strong></summary>

The HMAC is computed with a tenant secret from The Wallet Crew. This keeps the identifier exchange tamper-proof and prevents enumeration. The secret is usually stored in Adobe Commerce deployment config (`app/etc/env.php`) or as an environment variable and never exposed to the storefront.

</details>

<details>

<summary><strong>Can the same approach be used for gift cards?</strong></summary>

Yes. The same Magento wallet gift card pattern works when the gift card is issued as a pass type in The Wallet Crew and a stable gift card identifier exists in Adobe Commerce (or in a connected system). The My Account area can render a button for each gift card by switching `data-neostore-passType` and the external identifier key/value to the gift card identifier, then signing that identifier with HMAC server-side.

</details>

<details>

<summary><strong>How can secret rotation be handled?</strong></summary>

A rotation plan typically uses a short overlap window. During the overlap window, the backend can accept both the old and the new secret for HMAC validation. After the window, only the new secret remains active.

</details>

<details>

<summary><strong>Can the button be displayed somewhere else than the account page?</strong></summary>

Yes. The same SDK can be used on any authenticated page where a stable identifier is available server-side. Common examples include a gift card list page, a loyalty dashboard, or a pick & collect page when a pickup pass is used as an in-store scan token.

</details>
