> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zenobank.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Wix

> Accept crypto payments on your Wix store with a custom Zeno Bank payment provider.

<iframe width="100%" height="400" src="https://www.youtube.com/embed/t7RSAB5LBSM" title="Wix Tutorial" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen />

Register this webhook URL in the [Zeno Bank Dashboard](https://dashboard.zenobank.io/developer):

<Tabs>
  <Tab title="Custom domain">
    ```
    https://<yourdomain.com>/_functions/updateZenoBankTransaction
    ```

    Example: `https://zenobank.io/_functions/updateZenoBankTransaction`
  </Tab>

  <Tab title="Free Wix domain">
    ```
    https://<your-site>.wixsite.com/<site-name>/_functions/updateZenoBankTransaction
    ```

    Example: `https://zenobankio.wixsite.com/my-site-2/_functions/updateZenoBankTransaction`
  </Tab>
</Tabs>

***

This is the code you need to copy:

<Tabs>
  <Tab title="zeno-bank-config.js">
    ```javascript zeno-bank-config.js theme={null}
    import * as paymentProvider from 'interfaces-psp-v1-payment-service-provider';

    // ─────────────────────────────────────────────────────────────
    // CUSTOMIZE HERE (optional)
    // Edit these values to change how your payment method appears
    // on the dashboard and at checkout.
    // ─────────────────────────────────────────────────────────────
    const PAYMENT_METHOD_TITLE = 'Pay with Crypto';

    const LOGOS = {
      white: {
        svg: 'https://cryptologos.zenobank.io/library/tether-icon-dark.svg',
        png: 'https://cryptologos.zenobank.io/library/tether-icon-dark.png',
      },
      colored: {
        svg: 'https://cryptologos.zenobank.io/library/tether-icon-light.png',
        png: 'https://cryptologos.zenobank.io/library/tether-icon-light.svg',
      },
    };
    // ─────────────────────────────────────────────────────────────

    /** @returns {import('interfaces-psp-v1-payment-service-provider').PaymentServiceProviderConfig} */
    export function getConfig() {
      return {
        title: 'Zeno Bank',
        paymentMethods: [{
          hostedPage: {
            title: PAYMENT_METHOD_TITLE,
            billingAddressMandatoryFields: [],
            logos: LOGOS,
          },
        }],
        credentialsFields: [
          {
            simpleField: { name: 'apiKey', label: 'Zeno API Key' },
          },
          {
            simpleField: { name: 'webhookSecret', label: 'Webhook Signing Secret (whsec_...)' },
          },
        ],
      };
    }
    ```
  </Tab>

  <Tab title="zeno-bank.js">
    ```javascript zeno-bank.js theme={null}
    import * as paymentProvider from 'interfaces-psp-v1-payment-service-provider';
    import { secrets } from 'wix-secrets-backend.v2';
    import { elevate } from 'wix-auth';

    const ZENO_WEBHOOK_SECRET_NAME = 'ZENO_WEBHOOK_SECRET';

    const listSecretInfoElevated = elevate(secrets.listSecretInfo);
    const createSecretElevated = elevate(secrets.createSecret);
    const updateSecretElevated = elevate(secrets.updateSecret);

    /**
     * @param {import('interfaces-psp-v1-payment-service-provider').ConnectAccountOptions} options
     * @param {import('interfaces-psp-v1-payment-service-provider').Context} context
     * @returns {Promise<import('interfaces-psp-v1-payment-service-provider').ConnectAccountResponse | import('interfaces-psp-v1-payment-service-provider').BusinessError>}
     */
    export const connectAccount = async (options, context) => {
      const { apiKey, webhookSecret } = options.credentials;

      if (!webhookSecret) {
        return {
          reasonCode: 2009,
          errorCode: 'INVALID_CREDENTIALS',
          errorMessage: 'Webhook signing secret is required',
        };
      }

      const list = await listSecretInfoElevated();
      const existing = list.secrets.find((s) => s.name === ZENO_WEBHOOK_SECRET_NAME);
      const payload = { name: ZENO_WEBHOOK_SECRET_NAME, value: webhookSecret };

      if (existing) {
        await updateSecretElevated(existing._id, payload);
      } else {
        await createSecretElevated(payload);
      }

      return {
        credentials: { apiKey },
        accountId: 'zenobank',
        accountName: 'Zeno Bank',
      };
    };

    function minorToMajor(amount, currency) {
      const fractionDigits = new Intl.NumberFormat('en', {
        style: 'currency',
        currency,
      }).resolvedOptions().maximumFractionDigits;

      if (fractionDigits === undefined || fractionDigits === null) {
        throw new Error(`Fraction digits not found for currency ${currency}`);
      }
      return +amount / Math.pow(10, fractionDigits);
    }

    /**
     * @param {import('interfaces-psp-v1-payment-service-provider').CreateTransactionOptions} options
     * @param {import('interfaces-psp-v1-payment-service-provider').Context} context
     * @returns {Promise<import('interfaces-psp-v1-payment-service-provider').CreateTransactionResponse | import('interfaces-psp-v1-payment-service-provider').BusinessError>}
     */
    export const createTransaction = async (options, context) => {
      const { totalAmount, currency } = options.order.description;
      const { successUrl } = options.order.returnUrls;

      const request = await fetch('https://api.zenobank.io/api/v1/checkouts', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-Api-Key': options.merchantCredentials.apiKey,
        },
        body: JSON.stringify({
          orderId: options.wixTransactionId,
          priceAmount: minorToMajor(totalAmount, currency),
          priceCurrency: currency,
          successRedirectUrl: successUrl,
        }),
      });

      const response = await request.json();
      return {
        pluginTransactionId: response.id,
        redirectUrl: response.checkoutUrl,
      };
    };

    /**
     * @param {import('interfaces-psp-v1-payment-service-provider').RefundTransactionOptions} options
     * @param {import('interfaces-psp-v1-payment-service-provider').Context} context
     * @returns {Promise<import('interfaces-psp-v1-payment-service-provider').CreateRefundResponse | import('interfaces-psp-v1-payment-service-provider').BusinessError>}
     */
    export const refundTransaction = async (options, context) => {
      return {
        pluginRefundId: Math.random().toString(36).slice(2) + Date.now().toString(36),
        reasonCode: 3022,
        errorCode: 'REFUND_NOT_ALLOWED',
        errorMessage: 'Refund not allowed',
      };
    };
    ```
  </Tab>

  <Tab title="http-functions.js">
    ```javascript http-functions.js theme={null}
    import wixPaymentProviderBackend from "wix-payment-provider-backend";
    import { secrets } from "wix-secrets-backend.v2";
    import { elevate } from "wix-auth";
    import { ok, forbidden, badRequest, serverError } from "wix-http-functions";
    import crypto from "crypto";

    const TOLERANCE_SECONDS = 5 * 60;
    const ZENO_WEBHOOK_SECRET_NAME = "ZENO_WEBHOOK_SECRET";

    const getSecretValueElevated = elevate(secrets.getSecretValue);

    function readHeader(headers, primary, fallback) {
      return headers[primary] ?? headers[fallback] ?? null;
    }

    async function verifyZenoWebhook(request) {
      const rawBody = await request.body.text();

      const webhookId = readHeader(request.headers, "svix-id", "zeno-id");
      const webhookTimestamp = readHeader(request.headers, "svix-timestamp", "zeno-timestamp");
      const webhookSignature = readHeader(request.headers, "svix-signature", "zeno-signature");

      if (!webhookId || !webhookTimestamp || !webhookSignature) return null;

      const ts = parseInt(webhookTimestamp, 10);
      const now = Math.floor(Date.now() / 1000);
      if (isNaN(ts) || Math.abs(now - ts) > TOLERANCE_SECONDS) return null;

      const { value } = await getSecretValueElevated(ZENO_WEBHOOK_SECRET_NAME);
      let secret = value;
      if (secret.startsWith("whsec_")) secret = secret.slice(6);
      const keyBytes = Buffer.from(secret, "base64");

      const signedContent = `${webhookId}.${webhookTimestamp}.${rawBody}`;
      const expectedSig = crypto
        .createHmac("sha256", keyBytes)
        .update(signedContent)
        .digest("base64");

      const expectedBuf = Buffer.from(expectedSig);

      for (const versioned of webhookSignature.split(" ")) {
        const [version, sig] = versioned.split(",");
        if (version !== "v1" || !sig) continue;

        const receivedBuf = Buffer.from(sig);
        if (
          receivedBuf.length === expectedBuf.length &&
          crypto.timingSafeEqual(receivedBuf, expectedBuf)
        ) {
          return JSON.parse(rawBody);
        }
      }

      return null;
    }

    export async function post_updateZenoBankTransaction(request) {
      console.log("Zeno webhook received");

      const response = { headers: { "Content-Type": "application/json" } };

      let event;
      try {
        event = await verifyZenoWebhook(request);
      } catch (err) {
        console.error("Verification error:", err);
        return serverError(response);
      }

      if (!event) {
        return forbidden({ ...response, body: { error: "Invalid signature" } });
      }

      if (event.type !== "checkout.completed") {
        return ok(response);
      }

      const wixTransactionId = event.data.orderId;
      const pluginTransactionId = event.data.id;

      if (!wixTransactionId || !pluginTransactionId) {
        return badRequest({ ...response, body: { error: "Missing transaction IDs" } });
      }

      try {
        await wixPaymentProviderBackend.submitEvent({
          event: {
            transaction: { wixTransactionId, pluginTransactionId },
          },
        });
      } catch (err) {
        console.error("submitEvent failed:", err);
        return serverError(response);
      }

      return ok(response);
    }
    ```
  </Tab>
</Tabs>
