Actalink Docs
Usage/Acta billings

Subscribing to a subscription

This tutorial provides an overview of how consumers can subscribe to a subscription plan. Before proceeding, it is assumed that the developer is already familiar with the Acta Billings API and its functionality.

This guide is intended for registered merchants only. However, developers who wish to explore it can request access to a sandbox API key through our Help & Support section.

Subscription Steps

Create Checkout-session

In the first step, create the checkout session for consumer using PaymentLink Id

src/pages/index.tsx
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { usePathname } from "next/navigation";
import { ACTA_API_KEY, ACTA_BASE_URL } from "../utils";
 
export default function PaymentLink() {
  const path = usePathname();
  const router = useRouter();
 
  async function createCheckoutSession(paymentId: string): Promise<any> {
    const res = await fetch(`${ACTA_BASE_URL}/createcheckoutsession`, {
      method: "POST",
      body: JSON.stringify({
        paymentId: paymentId,
      }),
      headers: {
        "Content-Type": "application/json",
        "x-api-key": ACTA_API_KEY,
      },
    });
    const jsonRes = await res.json();
    const sessionId = jsonRes.sessionId;
    return sessionId;
  }
 
  useEffect(() => {
    const createCheckout = async () => {
      if (path !== null) {
        const paymentId = path.split("/");
        const sessionId = await createCheckoutSession(paymentId[2]);
        router.push(`/checkout-session/${sessionId}`);
      }
    };
    createCheckout();
  }, [path]);
 
  return (
    <div className="flex items-center justify-center h-screen text-lg font-semibold">
      Loading...
    </div>
  );
}

Create wagmi config

create a Wagmi configuration file.

src/wagmi.ts
 
import { http, createConfig } from "wagmi";
import { polygon } from "wagmi/chains";
 
export const config = createConfig({
chains: [polygon],
transports: {
[polygon.id]: http(),
},
});
Projects built with Rainbowkit can utilize Rainbowkit's default Wagmi configuration.

Precompute smart wallet address

Use the useActaAccount() hook to precompute the smart wallet address by passing the required parameters. Mock validators can be used for development and testing, but for production, please contact us to obtain custom validators.

The useSalt() hook requests a signature to generate a unique salt, which is then used to precompute the smart wallet address

src/pages/checkout-session/index.tsx
import {useAccount} from "wagmi";
import { useActaAccount, useFees, useMerkleSignUserOps, useSalt } from "@actalink/react-hooks";
import { config } from "../../wagmi";
 
export default function CheckoutSession() {
 
const { address: eoaAddress, status: eoaStatus, chainId } = useAccount();
 
const { salt } = useSalt({ eoaAddress, eoaStatus, config }); // this hook generates a unique salt using wallet signature
 
const validators = ["0x", "0x"]; // use your own validators
 
const { address: swAddress, status: swStatus, actaAccount } = useActaAccount({ eoaAddress, eoaStatus, chainId, config, validators });
 
return ()
}

Insert required parameters

Pass the required parameters, such as Paymaster URL, Paymaster Address, Token Address, etc. Additionally, useFees() is required for fee calculation, useMerkleSignUserOps() is needed for the Merkle operations signing process and useNonceKeys() is need to get unused validators.

src/pages/checkout-session/index.tsx
import {useAccount} from "wagmi";
import { useActaAccount, useFees, useMerkleSignUserOps, useSalt } from "@actalink/react-hooks";
import { config } from "../../wagmi";
 
export default function CheckoutSession() {
 
const { address: eoaAddress, status: eoaStatus, chainId } = useAccount();
 
const PAYMASTER_URL = "<paymaster url>";
 
const PAYMASTER_ADDRESS = "<paymaster address>"
 
const TOKEN_ADDRESS = "<erc20 token address">
 
const { salt } = useSalt({ eoaAddress, eoaStatus, config }); // this hook generates a unique salt using wallet signature
 
const validators = ["0x", "0x"]; // use your own validators
 
const { address: swAddress, status: swStatus, actaAccount } = useActaAccount({ eoaAddress, eoaStatus, chainId, config, validators });
 
const { calculateActaFees, getActaFeesRecipients, getPaymasterfees } = useFees({ config });
 
const { createERC20Transfer } = useMerkleSignUserOps({ eoaAddress: eoaAddress, config });
 
const { getPendingNonceKeys } = useNonceKeys();
 
return ()
}

Fetch Checkout-session data

Pass the required parameters, such as Paymaster URL, Paymaster Address, Token Address, etc. Additionally, useFees() is required for fee calculation, useMerkleSignUserOps() is needed for the Merkle operations signing process and useNonceKeys() is need to get unused validators.

src/pages/checkout-session/index.tsx
export default function CheckoutSession() {
 
...
const { getPendingNonceKeys } = useNonceKeys();
 
const [details, setDetails] = useState(null);
const [sessionId, setSessionId] = useState("");
 
  async function getCheckoutSessionData(ssnId: string): Promise<any> {
    const res = await fetch(
      `${ACTA_BASE_URL}/checkout-session?sessionId=${ssnId}`,
      {
        method: "GET",
        headers: {
          "Content-Type": "application/json",
          "x-api-key": ACTA_API_KEY,
        },
      }
    );
    const jsonRes = await res.json();
 
    return jsonRes;
  }
 
  useEffect(() => {
    const createCheckout = async () => {
      if (path !== null) {
        const sessionId = path.split("/");
        const session = await getCheckoutSessionData(sessionId[2]);
        setSessionId(sessionId[2]);
        setDetails(session.data);
      }
    };
    createCheckout();
  }, [path]);
 
 
return ()
}

Subscribe to subscription plan

In the final step, subscribe to a subscription plan, assuming the subscriber has selected plan[0].

src/pages/checkout-session/index.tsx
 
export default function CheckoutSession() {
 
...
const { getPendingNonceKeys } = useNonceKeys();
 
const [details, setDetails] = useState(null);
const [sessionId, setSessionId] = useState("");
 
...
 
const createERC20RecurringPayment = async (
    recipientAddr: Address,
    executionTimes: Array<number>,
    amount: bigint,
    times: number
  ) => {
    try {
      if (actaAccount === undefined) {
        return;
      }
      const paymasterAddress = PAYMASTER_ADDRESS;
       / Fetch the list of unused validators. A higher number of validators allows for more subscription.
      const unusedValidators = await getPendingNonceKeys(
        PAYMASTER_URL,
        validators,
        salt as Hex
      );
      if (unusedValidators === undefined || unusedValidators.length === 0) {
        throw new Error("Subscribe limit exceed");
      }
 
       // Read value of Fees charged by Actalink
      const actaFees = await calculateActaFees(amount, unusedValidators[0]);
      // Read value of Fees charged by Paymaster
      const paymasterFees = await getPaymasterfees(unusedValidators[0]);
      // Read the addresses of Actalink Fee receiver and paymaster Fee receiver
      const { actaFeesRecipient, paymasterFeesRecipient } = await getActaFeesRecipients(unusedValidators[0]);
      const userOps: Array<UserOperation<"0.7">> = [];
      const { factory, factoryData } = await actaAccount.getFactoryArgs();
      // Read latest nonce value of smart account for unusedValidators[0]
      const nonce = await actaAccount.getValidatorNonce(unusedValidators[0]);
      if (swAddress && actaFees !== undefined && nonce) {
        const transferData = await createTransferCallData(
          eoaAddress, // EOA address of owner or spender
          recipientAddr, // Receiver address
          TOKEN_ADDRESS, // ERC20 token address
          amount, // ERC20 token transfer amount. Amount should be parsed in token decimal units
          actaFees, // value of Actalink fees
          paymasterFees, // value of Paymaster fees
          actaFeesRecipient, // Address of Actalink fee receiver
          paymasterFeesRecipient // Address of Paymaster fee receiver
        );
 
        for (let i = 0; i < times; i++) {
          const preOp: UserOperation<"0.7"> = {
            ...getDefaultUserOp(),
            sender: swAddress as Address,
            nonce: nonce + BigInt(i),
            factory: i === 0 && factoryData ? factory : undefined,
            factoryData: i === 0 && factoryData ? factoryData : undefined,
            callData: transferData,
            paymaster: paymasterAddress,
            paymasterData: encodePacked(
              ["address", "uint128", "uint128"],
              [paymasterAddress, 100000n, 500000n]
            ),
          };
          // Request Paymaster to sponsor the transfer
          const sponsoredUserOp = await toSignedPaymasterData(
            `${PAYMASTER_URL}/api/sign/v2`,
            preOp
          );
          const userOp: UserOperation<"0.7"> = {
            ...sponsoredUserOp,
            paymaster: PAYMASTER_ADDRESS,
          };
          userOps.push(userOp);
        }
      }
      // Function used to sign the operations
      await createERC20Transfer({
        userOps: userOps,
        executionTimes: executionTimes,
        paymasterUrl: PAYMASTER_URL,
        paymentType: "subscription",
        paymentTypeParams: {
          subscriberId: uuidv4(), // unique address of subscriber
          owner: eoaAddress as Address, // eoa address of owner
          planId: details.subscription.plan[0].id, // id of selected plan
          subscriptionId: details.subscription.id, // subscriptionId for existing checkout session
          paylinkUrl: "", // payment link url for existing checkout session
          sessionId: sessionId, // sessionId for existing checkout session
        },
      });
    } catch (error) {
      console.error("Error in createERC20RecurringPayment: ", error);
    }
  };
 
 
  const handleSubscribe = async () => {
    if (details === null) return;
    const execTimes = generateExecutionTimes( // get the formula to calculate execution times array from here
      Date.now() + 3 * 60 * 1000,
      details.subscription.plan[0].frequency,
      details.subscription.plan[0].volume
    );
    const usdcAmount = parseUnits(
      details.subscription.plan[0].price,
      details.subscription.tokens[0].decimals
    );
    await createERC20RecurringPayment(
      details.subscription.receivers[0].address,
      execTimes,
      usdcAmount,
      details.subscription.plan[0].volume
    );
  };
 
 
return (
  <button onClick={(e) => { handleSubscribe() }}>Subscribe</button>
)
}

NOTE: Before subscribing to a subscription, the consumer must ensure that their smart wallet has sufficient allowance.

The required allowance should be calculated as:

totalAllowanceRequired = (transferAmount + actaFees + paymasterFees) * volume

Refer to this code example to calculate the required allowance.

👏 Congratulations! You have successfully subscribed to a subscription plan!

Full Code

import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { usePathname } from "next/navigation";
import { ACTA_API_KEY, ACTA_BASE_URL } from "../utils";
 
export default function PaymentLink() {
const path = usePathname();
const router = useRouter();
 
async function createCheckoutSession(paymentId: string): Promise<any> {
  const res = await fetch(`${ACTA_BASE_URL}/createcheckoutsession`, {
    method: "POST",
    body: JSON.stringify({
      paymentId: paymentId,
    }),
    headers: {
      "Content-Type": "application/json",
      "x-api-key": ACTA_API_KEY,
    },
  });
  const jsonRes = await res.json();
  const sessionId = jsonRes.sessionId;
  return sessionId;
}
 
useEffect(() => {
  const createCheckout = async () => {
    if (path !== null) {
      const paymentId = path.split("/");
      const sessionId = await createCheckoutSession(paymentId[2]);
      router.push(`/checkout-session/${sessionId}`);
    }
  };
  createCheckout();
}, [path]);
 
return (
  <div className="flex items-center justify-center h-screen text-lg font-semibold">
    Loading...
  </div>
);
}

If you want to see the complete code that integrates all the previous steps in detail, you can find it in our separate repository. If you plan to run it, don’t forget to replace the API key with your own!

On this page