Arkena Docs

Detect the Arkena provider

A reusable, SSR-safe helper for finding window.arkena across both already-injected and not-yet-injected states.

The Arkena wallet injects window.arkena (and window.canton) into every browser tab where the extension is enabled. Injection is asynchronous, so detection has to handle two cases: the script ran before your code, or your code ran first.

  1. Synchronous case

    If the wallet has already injected, window.arkena is set. A one-liner is enough:

    Code
    const provider = typeof window !== "undefined" ? window.arkena : null;

    Server-side, window is undefined — always check first. SSR-rendered components need a useEffect boundary.

  2. Asynchronous case

    If your code runs before the extension's content script, listen for the injection event. The wallet dispatches arkena#initialized once on injection (and canton#initialized for CIP-103 compatibility).

    Code
    function waitForArkena(timeoutMs = 1000): Promise<typeof window.arkena | null> {
      if (typeof window === "undefined") return Promise.resolve(null);
      if (window.arkena) return Promise.resolve(window.arkena);
      return new Promise((resolve) => {
        const onReady = () => resolve(window.arkena);
        window.addEventListener("arkena#initialized", onReady, { once: true });
        setTimeout(() => resolve(window.arkena ?? null), timeoutMs);
      });
    }
  3. CIP-103 multi-wallet discovery

    The provider also follows CIP-103 (the EIP-6963 pattern): on every canton:requestProvider event from the page, the wallet dispatches a canton:announceProvider event with its identity. Wallet-agnostic libraries use this to discover all installed Canton wallets without conflicting on the global window namespace.

    Code
    const wallets: Array<{ uuid: string; name: string; provider: unknown }> = [];
    window.addEventListener("canton:announceProvider", (e: Event) => {
      const detail = (e as CustomEvent).detail;
      wallets.push({ uuid: detail.uuid, name: detail.name, provider: detail.provider });
    });
    window.dispatchEvent(new Event("canton:requestProvider"));

    When only Arkena is installed, the discovery surface and window.arkena resolve to the same object.

A reusable helper

Drop this into src/lib/arkena.ts (or wherever your dApp keeps its provider helpers):

src/lib/arkena.ts

Code
type ArkenaProvider = {
  isArkena: true;
  isCanton: true;
  request: (args: { method: string; params?: Record<string, unknown> }) => Promise<unknown>;
  on: (event: string, handler: (...args: unknown[]) => void) => void;
  removeListener: (event: string, handler: (...args: unknown[]) => void) => void;
};

declare global {
  interface Window {
    arkena?: ArkenaProvider;
  }
}

export async function getArkenaProvider(timeoutMs = 1000): Promise<ArkenaProvider | null> {
  if (typeof window === "undefined") return null;
  if (window.arkena) return window.arkena;
  return new Promise((resolve) => {
    const onReady = () => resolve(window.arkena ?? null);
    window.addEventListener("arkena#initialized", onReady, { once: true });
    setTimeout(() => resolve(window.arkena ?? null), timeoutMs);
  });
}

SSR boundary

In Next.js App Router, detection has to run on the client. Put the client-only logic behind 'use client' and useEffect:

Code
"use client";
import { useEffect, useState } from "react";
import { getArkenaProvider } from "@/lib/arkena";

export function ArkenaStatus() {
  const [present, setPresent] = useState<boolean | null>(null);
  useEffect(() => {
    void getArkenaProvider().then((p) => setPresent(Boolean(p)));
  }, []);
  if (present === null) return null;
  return present ? <span>Arkena detected</span> : <span>Install Arkena</span>;
}

What's next