/* eslint-disable no-use-before-define */
import PropTypes from 'prop-types';
import omit from 'lodash/omit';
import pick from 'lodash/pick';
import cloneDeep from 'lodash/cloneDeep';
import isFunction from 'lodash/isFunction';
import isUndefined from 'lodash/isUndefined';
import defaultsDeep from 'lodash/defaultsDeep';
import Cookie from 'js-cookie';
import EventEmitter from 'eventemitter3';
import pWaitFor from 'p-wait-for';
import { Box, CircularProgress, Typography, useTheme } from '@mui/material';
import { getVisitorId, setVisitorId, ensureVisitorId, isUrl } from '@arcblock/ux/lib/Util';
import Toast, { ToastProvider } from '@arcblock/ux/lib/Toast';
import Center from '@arcblock/ux/lib/Center';
import { useCreation, useLatest, useMemoizedFn, useMount, usePrevious, useReactive, useUpdateEffect } from 'ahooks';
import { joinURL } from 'ufo';
import { Icon } from '@iconify/react';
import KeyboardDoubleArrowRightRoundedIcon from '@iconify-icons/material-symbols/keyboard-double-arrow-right-rounded';
import useBrowser from '@arcblock/react-hooks/lib/useBrowser';
import useConfirm from '@arcblock/ux/lib/Dialog/use-confirm';
import DID from '@arcblock/ux/lib/DID';
import Avatar from '@arcblock/ux/lib/Avatar';
import { getCurrentAppPid } from '@arcblock/ux/lib/SessionUser/libs/utils';
import { translate } from '@arcblock/ux/lib/Locale/util';
import { BlockletSDK, getBlockletSDK } from '@blocklet/js-sdk';
import bridge from '@arcblock/bridge';
import { getFederatedEnabled } from '@arcblock/ux/lib/Util/federated';
import noop from 'lodash/noop';
import isNil from 'lodash/isNil';

import { useState } from 'react';
import createStorage from '../Storage';
import {
  getAppId,
  updateConnectedInfo,
  getBrowserLang,
  formatCacheTtl,
  sleep,
  decodeUrlParams,
  logger,
  debug,
} from '../utils';
import WindowFocusAware from './window-focus-aware';
import { OAuthProvider, useOAuth, OAuthConsumer, OAuthContext } from '../OAuth';
import { PasskeyProvider, usePasskey, PasskeyConsumer, PasskeyContext } from '../Passkey';
import { useDid, WrapDid } from '../User';
import useFederated from './hooks/use-federated';
import useSessionToken from './hooks/use-session-token';
import useProtectedRoutes from './hooks/use-protected-routes';
import { SessionContext } from './context';
import useConnect from '../Connect/use-connect';
import { EVENTS } from './libs/constants';
import { translations } from './libs/locales';
import { NotOpenError } from '../error';
import {
  API_DID_PREFIX,
  BLOCKLET_SERVICE_PATH_PREFIX,
  DEFAULT_TIMEOUT,
  REFRESH_TOKEN_STORAGE_KEY,
  SESSION_TOKEN_STORAGE_KEY,
  DID_SPACES_BASE_URL,
} from '../constant';
import { checkEnableAutoLogin, getMobileVisitorId, login as loginInMobile } from './libs/login-mobile';
import useQuickConnect from '../Connect/hooks/use-quick-connect';
import { didSpacesIsRequired } from './libs/did-spaces';
import { getWalletDid } from '../User/use-did';
import useDIDSpacesGuide from './did-spaces-guide';
import { FederatedProvider, useFederatedContext } from '../Federated';
import useVerify from './hooks/use-verify';

export * from './libs';

const sdk = getBlockletSDK();

const { Provider, Consumer } = SessionContext;

const didConnectTimeoutError = {
  en: 'DID Connect timeout, please reopen DID Connect popup',
  zh: 'DID Connect 超时, 请重新打开 DID Connect',
};

const waitForDidConnect = {
  en: 'DID Connect is already opened, please wait for it to complete',
  zh: 'DID Connect 已打开，请等待完成',
};

export default function createSessionContext(
  storageKey = SESSION_TOKEN_STORAGE_KEY,
  storageEngine = 'ls',
  storageOptions = {},
  opts = {}
) {
  let defaultServiceHost = '/';
  // FIXME: @zhanghan 当 connect 组件使用不同的 baseUrl 时，不应该直接从 window.blocklet 去取值
  if (globalThis?.blocklet?.prefix) {
    defaultServiceHost = globalThis.blocklet.prefix;
  }
  if (typeof opts === 'boolean') {
    // eslint-disable-next-line no-param-reassign
    opts = {
      appendAuthServicePrefix: opts,
      extraParams: {},
    };
  }

  defaultsDeep(opts, {
    rolling: true,
    appendAuthServicePrefix: false,
    extraParams: {},
    refreshTokenStorageKey: REFRESH_TOKEN_STORAGE_KEY,
  });

  function notifyBridge(sessionState) {
    if (globalThis.blocklet) {
      if (sessionState?.user) {
        const data = {
          did: sessionState.user.did,
          host: window.location.host,
          appPid: globalThis.blocklet.appPid,
          visitorId: getVisitorId(),
          sourceAppPid: sessionState.user.sourceAppPid,
          fullName: sessionState.user.fullName,
        };
        debug('bridge callArc: onLogin', data);
        bridge.callArc('onLogin', { ...data, user: data });
      } else {
        bridge.callArc('onLogin', { error: 'no user', code: 400 });
        debug('notifyBridge failed', { sessionState });
      }
    }
  }

  const sessionTokenStorage = createStorage(storageKey, storageEngine, storageOptions);

  const refreshTokenStorage = createStorage(opts.refreshTokenStorageKey, 'ls');

  function SessionProvider({ ...rawProps }) {
    const props = Object.assign({}, rawProps);
    if (isUndefined(props.serviceHost)) {
      props.serviceHost = defaultServiceHost;
    }
    if (isUndefined(props.locale)) {
      props.locale = '';
    }
    if (isUndefined(props.action)) {
      props.action = 'login';
    }
    if (isUndefined(props.prefix)) {
      props.prefix = API_DID_PREFIX;
    }
    if (isUndefined(props.appendAuthServicePrefix)) {
      props.appendAuthServicePrefix = false;
    }
    if (isUndefined(props.extraParams)) {
      props.extraParams = {};
    }
    if (isUndefined(props.options)) {
      props.options = {};
    }
    if (isUndefined(props.autoConnect)) {
      props.autoConnect = false;
    }
    if (isUndefined(props.autoDisconnect)) {
      props.autoDisconnect = true;
    }
    if (isUndefined(props.useSocket)) {
      props.useSocket = true;
    }
    if (isUndefined(props.timeout)) {
      props.timeout = DEFAULT_TIMEOUT * 1000;
    }
    if (isUndefined(props.webWalletUrl)) {
      props.webWalletUrl = undefined;
    }
    if (isUndefined(props.protectedRoutes)) {
      props.protectedRoutes = ['*'];
    }
    if (isUndefined(props.apiOptions)) {
      props.apiOptions = {};
    }
    if (isUndefined(props.lazyRefreshToken)) {
      props.lazyRefreshToken = false;
    }

    const { connectApi, connectHolder } = useConnect();
    const [unReadCount, setUnReadCount] = useState(0);
    const browser = useBrowser();
    const { requestStorageAccess } = useFederatedContext();
    const { palette } = useTheme();
    const searchParams = new URLSearchParams(window.location.search);

    /**
     * @typedef PageState
     * @property {object} extraParams - 额外参数
     * @property {object} options - 配置项
     * @property {string} currentLocale - 当前语言
     * @property {boolean} allowWallet - 是否显示钱包操作入口
     * @property {('popup'|'window'|'iframe')} [openMode] - 打开的方式
     * @property {boolean} autoConnect - 是否自动连接
     * @property {string} prefix - 自动生成的当前应用的 prefix
     */
    /**
     * 页面状态
     * @type {PageState}
     */
    const pageState = useReactive({
      extraParams: {},
      options: {},
      currentLocale: props.locale,
      allowWallet: undefined,
      openMode: 'window',
      get autoConnect() {
        // for backward compatibility
        if (typeof props.autoConnect === 'boolean') {
          return props.autoConnect;
        }
        // eslint-disable-next-line react/prop-types
        return !!props.autoLogin;
      },
      get prefix() {
        if (opts.appendAuthServicePrefix || props.appendAuthServicePrefix) {
          return joinURL(BLOCKLET_SERVICE_PATH_PREFIX, props.prefix);
        }
        return props.prefix;
      },
      get notificationPrefix() {
        const _prefix = '/api/notifications';
        if (opts.appendAuthServicePrefix || props.appendAuthServicePrefix) {
          return joinURL(BLOCKLET_SERVICE_PATH_PREFIX, _prefix);
        }
        return _prefix;
      },
    });

    const state = useReactive({
      action: props.action,
      error: '',
      initialized: false,
      loading: false,
      open: false,
      user: null,
      provider: '',
      walletOS: '',
      baseUrl: '',
      unReadCount: 0,
      // 不可以直接个性 props.autoConnect (readonly)
    });
    const currentLocale = useCreation(() => {
      return props.locale || pageState.currentLocale || getBrowserLang();
    }, [props.locale, pageState.currentLocale]);

    const { confirmApi, confirmHolder } = useConfirm();

    const stopOnOpen = useMemoizedFn(() => {
      if (state.open && !browser.arcSphere) {
        const msg = waitForDidConnect[currentLocale] || waitForDidConnect.en;
        Toast.warning(msg);
        throw new Error(msg);
      }
    });

    const events = useCreation(() => new EventEmitter(), []);
    /**
     * 用于包装 did-connect 的 open 和 close 事件，添加一些默认的行为
     * 1. 在 open 中添加的事件订阅，did-connect close 时需要取消订阅
     * 2. session 内部的事件订阅使用与外部事件不同命的名字
     * @param {string} eventName - 订阅的事件名
     * @param {function} doneFn - 订阅事件被调用时，执行的函数
     * @param {function} cancelFn - 关闭 did-connect 时，执行的回调
     * @returns
     */
    const wrapOnceEmit = useMemoizedFn((eventName, doneFn, cancelFn) => {
      const fnKeyMap = {
        login: [EVENTS.LOGIN, EVENTS.CANCEL_LOGIN],
        'bind-wallet': [EVENTS.BIND_WALLET, EVENTS.CANCEL_BIND_WALLET],
        'switch-passport': [EVENTS.SWITCH_PASSPORT, EVENTS.CANCEL_SWITCH_PASSPORT],
        'switch-profile': [EVENTS.SWITCH_PROFILE, EVENTS.CANCEL_SWITCH_PROFILE],
        // HACK: 对于内部来说，switch-did 就是 login，所以内部的监听事件仍然是 login
        'switch-did': [EVENTS.LOGIN, EVENTS.CANCEL_LOGIN],
        // @FIXME: @zhanghan 整个事件发射的机制需要重构，目前的实现不是很优雅，对于用户来说不是很好用。 https://github.com/ArcBlock/ux/pull/1414#discussion_r1904830954
        'did-space-connected': [EVENTS.DID_SPACE_CONNECTED],
      };

      if (!Object.keys(fnKeyMap).includes(eventName)) {
        return;
      }

      const listenFn = async (...args) => {
        if (isFunction(doneFn)) {
          await doneFn(...args);
        }
        events.emit(eventName, ...args);
      };
      events.once(fnKeyMap[eventName][0], listenFn);
      events.once(fnKeyMap[eventName][1], async (...args) => {
        if (isFunction(listenFn)) {
          events.off(fnKeyMap[eventName][0], listenFn);
        }
        if (isFunction(cancelFn)) {
          await cancelFn(...args);
        }
        events.emit(`cancel-${eventName}`, ...args);
      });
    });

    const prevInitialized = usePrevious(state.initialized, (prev, next) => {
      if (prev !== next) {
        return true;
      }
      if (prev === true && next === true) {
        return true;
      }
      return false;
    });

    const {
      syncSessionSate,
      handleRefreshUser,
      handleRefreshToken,
      renewToken,
      clearSession,
      handleLoginResult,
      decrypt,
      service,
      getSessionToken,
      getRefreshToken,
      setRefreshToken,
      setSessionToken,
    } = useSessionToken({
      state,
      pageState,
      sessionTokenStorage,
      refreshTokenStorage,
      serviceHost: props.serviceHost,
      apiOptions: props.apiOptions,
      lazyRefreshToken: props.lazyRefreshToken,
      onRefresh({ type, sessionToken, refreshToken, user }) {
        debug('onRefresh', { type, user });

        if (globalThis.blocklet) {
          const data = {
            did: user.did,
            host: window.location.host,
            appPid: globalThis.blocklet.appPid,
            visitorId: getVisitorId(),
            sourceAppPid: user.sourceAppPid,
            fullName: user.fullName,
          };
          if (type === 'refreshToken') {
            debug('bridge callArc: onRefreshToken');
            bridge.callArc('onRefreshToken', {
              sessionToken,
              refreshToken,
              user: data,
            });
          } else if (type === 'refreshUser') {
            debug('bridge callArc: onRefreshUser');
            bridge.callArc('onRefreshUser', {
              sessionToken,
              refreshToken,
              user: data,
            });
          }
        }
      },
    });

    const handleVerify = useVerify({ state, currentLocale, connectApi });

    const loginWallet = useMemoizedFn((done, extraParams = {}, options = {}) => {
      stopOnOpen();
      const params = done;
      if (typeof params === 'object') {
        done = params.onSuccess; // eslint-disable-line no-param-reassign
        extraParams = params.extraParams || {}; // eslint-disable-line no-param-reassign
        options = params.options || {}; // eslint-disable-line no-param-reassign
        // FIXME: @zhanghan 将来需要移除这种传递方式，配置参数统一放到 options 中
        options.origin = params.origin || ''; // eslint-disable-line no-param-reassign
      }

      if (!state.user || options.origin === 'switch-did') {
        if (options.origin === 'switch-did') {
          wrapOnceEmit('switch-did', done, params?.onCancel);
        } else {
          wrapOnceEmit('login', done, params?.onCancel);
        }

        pageState.extraParams = extraParams;
        pageState.options = options;
        state.action = 'login';
        openConnect();
      }
    });

    // 高阶函数包装器 - 二次认证装饰器
    const withSecondaryAuth = useMemoizedFn((fn, options = {}) => {
      // 保存原始函数和上下文
      const originalFn = fn;

      return function wrappedSecondaryAuth(...args) {
        // 保存调用时的 this 上下文
        const context = this;

        return (async () => {
          try {
            const result = await handleVerify(options);
            if (!result.sessionId) {
              throw new Error('Authentication failed');
            }
            // 使用 call 方法确保 this 指向正确
            return await originalFn.call(context, result, ...args);
          } catch (error) {
            // 如果是用户取消认证，不需要显示错误信息
            console.error('Authentication failed', error);
            throw error;
          }
        })();
      };
    });
    /**
     * did-connect 登录函数
     * @type {import('../types').LoginSessionFn}
     */
    const login = useMemoizedFn(async (done, extraParams = {}, options = {}) => {
      stopOnOpen();
      const params = done;
      if (typeof params === 'object') {
        done = params.onSuccess; // eslint-disable-line no-param-reassign
        extraParams = params.extraParams || {}; // eslint-disable-line no-param-reassign
        options = params.options || {}; // eslint-disable-line no-param-reassign
        // FIXME: @zhanghan 移除了 params.onCancel 回调，需要观察是否会导致别的问题，目前未找到任何地方使用了 onCancel 回调
      }

      if (isUndefined(extraParams?.inviter) && window.localStorage.getItem('inviter')) {
        extraParams.inviter = window.localStorage.getItem('inviter');
      }

      // 使用 Url 中编码的参数
      const { params: decodedParams } = await decodeUrlParams();
      if (isUndefined(extraParams?.forceConnected) && decodedParams?.forceConnected) {
        extraParams.forceConnected = decodedParams.forceConnected;
        if (isUndefined(options.showQuickConnect)) {
          options.showQuickConnect = false;
        }
      }
      if (isUndefined(extraParams?.sourceAppPid) && !isUndefined(decodedParams?.sourceAppPid)) {
        extraParams.sourceAppPid = decodedParams.sourceAppPid;
      }
      if (extraParams?.openMode === 'redirect') {
        const redirect = extraParams?.redirect || '/';
        const url = new URL(`${BLOCKLET_SERVICE_PATH_PREFIX}/login`, window.location.origin);
        url.searchParams.set('redirect', redirect);
        if (extraParams?.forceConnected) {
          url.searchParams.set('forceConnected', extraParams?.forceConnected);
        }
        if (!isUndefined(extraParams?.sourceAppPid)) {
          url.searchParams.set('sourceAppPid', extraParams?.sourceAppPid);
        }
        if (!isUndefined(extraParams?.inviter)) {
          url.searchParams.set('inviter', extraParams?.inviter);
        }
        if (options?.origin) {
          url.searchParams.set('origin', options.origin);
        }
        // 这里是安全的
        window.location.href = url.href;
        return;
      }

      let sourceAppPid = extraParams?.sourceAppPid;
      if (isUndefined(sourceAppPid) && federatedEnabled) {
        sourceAppPid = masterSite.appPid;
      }
      // 必须等到初始化完成后才能执行
      await pWaitFor(() => state.initialized);

      // @compatible: 兼容旧的传递 origin 的方式
      if (typeof options === 'string') {
        // eslint-disable-next-line no-param-reassign
        options = { origin: options };
      }
      // @compatible end

      const copyExtraParams = cloneDeep(extraParams);
      copyExtraParams.passkeyBehavior = 'both';

      // if (!isUndefined(copyExtraParams.allowWallet)) {
      //   pageState.allowWallet = copyExtraParams.allowWallet;
      //   delete copyExtraParams.allowWallet;
      // } else {
      //   pageState.allowWallet = undefined;
      // }
      pageState.allowWallet = undefined;

      const openMode = copyExtraParams?.openMode || props?.extraParams?.openMode;

      if (!isUndefined(openMode)) {
        pageState.openMode = openMode;
        // 如果处于统一登录模式下，并且当前应用不是 master，才需要使用 window.open 的方式
      } else if (sourceAppPid) {
        if (sourceAppPid !== globalThis.blocklet.appPid && getFederatedEnabled(globalThis.blocklet)) {
          pageState.openMode = 'window';
        }
      } else {
        pageState.openMode = 'popup';
      }

      // 如果已经有跨站存储的权限，并且当前是 window 模式，则强制改为 popup 模式
      if (pageState.openMode === 'window' && !browser.arcSphere && !browser.wallet) {
        const result = await requestStorageAccess();
        if (result) {
          pageState.openMode = 'popup';
        }
      }

      if (sourceAppPid) {
        state.baseUrl = globalThis.blocklet.appUrl;
      } else {
        state.baseUrl = '';
      }
      loginWallet(done, copyExtraParams, options);
    });

    // eslint-disable-next-line require-await
    const logout = useMemoizedFn(async (done) => {
      const visitorId = getVisitorId();

      if (globalThis.blocklet) {
        // TODO: 需要增加 webapp 退出登录的处理
        sdk.user.logout({ visitorId }).catch((error) => {
          console.warn('Failed to logout remote', error);
        });
        await sleep(100);
      }

      // HACK: 仅在 blocklet 环境中才发起请求
      if (globalThis.blocklet) {
        const logoutData = {
          visitorId,
          appPid: globalThis.blocklet.appPid,
          userDid: state.user.did,
        };
        debug('bridge callArc: onLogout', logoutData);
        bridge.callArc('onLogout', logoutData);
      }

      clearSession();
      closeConnect();
      state.user = null;
      state.provider = '';
      state.walletOS = '';
      state.error = '';
      state.loading = false;
      events.emit('logout');

      if (typeof done === 'function') {
        done();
      }
    });

    const {
      userSessions,
      refresh: refreshUserSessions,
      loaded: loadedUserSessions,
      loginUserSession,
    } = useQuickConnect({
      appPid: getCurrentAppPid(state.user),
      loginAppPid: globalThis?.blocklet?.appPid,
      sourceAppPid: state?.user?.sourceAppPid,
      autoFetch: false,
      fetchAll: true,
    });

    const ensureLogin = useMemoizedFn(async () => {
      if (!state?.user) {
        await new Promise((resolve) => {
          login(() => {
            resolve();
          });
        });
      }
    });

    // HACK: 先保留这段代码，后续改版时删除
    // const cleanConnectedState = useMemoizedFn(() => {
    //   const cookieOptions = getCookieOptions({ returnDomain: false });
    //   Cookie.remove('connected_did', cookieOptions);
    //   Cookie.remove('connected_pk', cookieOptions);
    //   Cookie.remove('connected_app', cookieOptions);
    //   Cookie.remove('connected_wallet_os', cookieOptions);
    // });

    // See:
    // - https://github.com/ArcBlock/ux/issues/520
    // - https://github.com/ArcBlock/did-connect/issues/56
    const switchDid = useMemoizedFn((done, extraParams = {}, options = {}) => {
      stopOnOpen();
      const params = done;
      if (typeof params === 'object') {
        done = params.onSuccess; // eslint-disable-line no-param-reassign
        extraParams = params.extraParams || {}; // eslint-disable-line no-param-reassign
        options = params.options || {}; // eslint-disable-line no-param-reassign
      }

      // @compatible: 兼容旧的传递 origin 的方式
      if (typeof options === 'string') {
        // eslint-disable-next-line no-param-reassign
        options = { origin: options };
      }
      // @compatible end

      if (options?.userSession) {
        loginUserSession(options.userSession).then((result) => {
          updateConnectedInfo(
            {
              connected_did: options.userSession.user.did,
              connected_pk: options.userSession.user.pk,
              connected_wallet_os: options.userSession.extra.walletOS,
              connected_app: getAppId(),
            },
            true
          );
          wrapOnceEmit('switch-did', done);
          handleLoginResult({ ...result, encrypted: false });
          handleRefreshUser({ showProgress: true }).then(async () => {
            await sleep(200);
            events.emit(EVENTS.LOGIN, result, decrypt, session.current);
            notifyBridge(session.current);
            await connectToDidSpaceForFullAccess();
          });
        });
      } else {
        if (isUndefined(extraParams?.forceConnected)) {
          // switchDid 时，禁用 forceConnected，以便切换到其他的钱包
          extraParams.forceConnected = false;
        }
        extraParams.passkeyBehavior = 'both';
        login(done, extraParams, { ...options, origin: 'switch-did' });
      }
    });

    const refreshProfile = useMemoizedFn(async () => {
      await sdk.user.refreshProfile();
    });

    const switchProfile = useMemoizedFn(async (done, extraParams = {}) => {
      await ensureLogin();
      stopOnOpen();
      const params = done;
      if (typeof params === 'object') {
        done = params.onSuccess; // eslint-disable-line no-param-reassign
        extraParams = params.extraParams || {}; // eslint-disable-line no-param-reassign
      }
      wrapOnceEmit('switch-profile', done, params?.onCancel);
      pageState.extraParams = extraParams;
      pageState.options = {};
      state.action = 'switch-profile';
      openConnect();
    });

    const switchPassport = useMemoizedFn(async (done, extraParams = {}) => {
      await ensureLogin();
      stopOnOpen();
      const params = done;
      if (typeof params === 'object') {
        done = params.onSuccess; // eslint-disable-line no-param-reassign
        extraParams = params.extraParams || {}; // eslint-disable-line no-param-reassign
      }
      wrapOnceEmit('switch-passport', done, params?.onCancel);

      pageState.extraParams = extraParams;
      pageState.options = {};
      state.action = 'switch-passport';
      openConnect();
    });

    const connectToDidSpace = useMemoizedFn(async ({ extraParams = {}, onSuccess = () => {}, ...rest } = {}) => {
      await ensureLogin();
      stopOnOpen();

      pageState.extraParams = {
        referrer: window.location.href,
        purpose: 'authorize-for-import',
        ...extraParams,
        saveConnect: false, // 连接 spaces 不保存连接信息
      };
      pageState.options = {};
      state.action = 'connect-to-did-spaces-for-user';
      openConnect({
        hideCloseButton: true,
        onSuccess,
        ...rest,
      });
    });

    /**
     * @description
     * @param {{
     *  extraParams: Record<string, any>,
     *  onSuccess: (response: Record<string, any>, decrypt: (value: string) => string),
     * }} [{ extraParams = {}, onSuccess = {} }={}]
     */
    const connectToDidSpaceForImport = useMemoizedFn(async ({ extraParams = {}, onSuccess = {} } = {}) => {
      await ensureLogin();
      connectToDidSpace({
        extraParams: {
          // service 交互
          purpose: 'authorize-for-import',
          ...extraParams,
        },
        onSuccess,
        hideCloseButton: false,
      });
    });

    const { didSpacesGuideApi, DidSpacesGuideView } = useDIDSpacesGuide({
      autoClose: false,
    });

    const connectToDidSpaceForFullAccess = async ({ extraParams = {}, onSuccess = () => {} } = {}) => {
      await ensureLogin();
      if (!(await didSpacesIsRequired(state?.user))) {
        return;
      }
      didSpacesGuideApi.open();
      didSpacesGuideApi.onConnect((callback = noop) => {
        const popupProps = {
          prefix: '/connect-to-did-space',
          custom: true,
          extraParams: {
            provider: 'wallet',
            appPid: window.blocklet?.appPid,
            appDid: window.blocklet?.appId,
            appName: window.blocklet?.appName,
            appDescription: window.blocklet?.appDescription,
            appUrl: window.blocklet?.appUrl,
            referrer: window.location.href,
            connectScope: 'user',
            ...extraParams,
          },
          onSuccess: async (response, decryptFunc) => {
            const spaceGateway = decryptFunc ? decryptFunc(response.spaceGateway) : response.spaceGateway;
            await new BlockletSDK().user.updateDidSpace({ spaceGateway });
            await handleRefreshUser({ showProgress: true });
            await onSuccess(response, decryptFunc);
            await didSpacesGuideApi.close();
            events.emit(EVENTS.DID_SPACE_CONNECTED, response, decryptFunc);
            if (globalThis.blocklet) {
              debug('bridge callArc: onDidSpaceConnected');
              bridge.callArc('onDidSpaceConnected');
            }
            callback(true);
          },
          onError: (error) => {
            console.error(error);
            Toast.error(error.message);
            didSpacesGuideApi.close();
            callback(false);
          },
          onClose: () => {
            callback();
          },
        };

        connectApi.openPopup(popupProps, {
          baseUrl: DID_SPACES_BASE_URL,
        });
      });
    };

    const bindWallet = useMemoizedFn(async (done, extraParams = {}) => {
      await ensureLogin();
      if (getWalletDid(state.user)) {
        done({}, decrypt, session.current);
        return;
      }
      stopOnOpen();
      const params = done;
      if (typeof params === 'object') {
        done = params.onSuccess; // eslint-disable-line no-param-reassign
      }
      wrapOnceEmit('bind-wallet', done, params?.onCancel);
      pageState.extraParams = extraParams;
      pageState.options = {};
      state.action = 'bind-wallet';
      openConnect();
    });

    const onSwitchPassport = useMemoizedFn(async (result) => {
      debug('onSwitchPassport', { result });
      pageState.extraParams = {};
      pageState.options = {};
      handleLoginResult(result);
      state.loading = false;
      await handleRefreshUser({ showProgress: true });
      await sleep(200);
      events.emit(EVENTS.SWITCH_PASSPORT, result, decrypt, session.current);
      if (globalThis.blocklet) {
        debug('bridge callArc: onSwitchPassport');
        bridge.callArc('onSwitchPassport');
      }
    });

    const onLogin = useMemoizedFn(async (result) => {
      if (state.action === 'switch-passport') {
        onSwitchPassport(result);
        return;
      }

      debug('onLogin', { result });
      pageState.extraParams = {};
      pageState.options = {};
      handleLoginResult(result);
      state.loading = false;
      await handleRefreshUser({ showProgress: true });
      await sleep(200);
      if (state.action === 'login') {
        debug('onLogin: emit LOGIN event', { result });
        events.emit(EVENTS.LOGIN, result, decrypt, session.current);
        notifyBridge(session.current);
        connectToDidSpaceForFullAccess();
      }
    });

    const onBindWallet = useMemoizedFn((result) => {
      pageState.extraParams = {};
      pageState.options = {};
      handleLoginResult(result);
      state.loading = false;
      handleRefreshUser({ showProgress: true }).then(async () => {
        await sleep(100);
        events.emit(EVENTS.BIND_WALLET, result, decrypt, session.current);
        if (globalThis.blocklet) {
          debug('bridge callArc: onBindWallet');
          bridge.callArc('onBindWallet');
        }
      });
      // NOTICE: auth0 绑定 wallet 在新的版本中，不会再要求切换 login_token，这里的处理是为了兼容有无 sessionToken 的两种情况，都可以顺利完成当前绑定操作
    });

    const onSwitchProfile = useMemoizedFn((result) => {
      debug('onSwitchProfile', { result });
      pageState.extraParams = {};
      pageState.options = {};
      state.loading = false;
      handleRefreshUser({ showProgress: true }).then(async () => {
        await sleep(100);
        events.emit(EVENTS.SWITCH_PROFILE, result, decrypt, session.current);
        if (globalThis.blocklet) {
          debug('bridge callArc: onSwitchProfile');
          bridge.callArc('onSwitchProfile');
        }
      });
    });

    const onClose = useMemoizedFn((action) => {
      pageState.extraParams = {};
      pageState.options = {};
      closeConnect();
      const fnMap = {
        login: EVENTS.CANCEL_LOGIN,
        'switch-profile': EVENTS.CANCEL_SWITCH_PROFILE,
        'switch-passport': EVENTS.CANCEL_SWITCH_PASSPORT,
        'bind-wallet': EVENTS.CANCEL_BIND_WALLET,
      };
      const fnCancelMap = {
        login: 'offLogin',
        'switch-profile': 'offSwitchProfile',
        'switch-passport': 'offSwitchPassport',
        'bind-wallet': 'offBindWallet',
      };
      events.emit(fnMap[action]);

      if (globalThis.blocklet) {
        debug(`bridge callArc: ${fnCancelMap[action]}`);
        bridge.callArc(`${fnCancelMap[action]}`);
      }
    });

    const onSuccess = useMemoizedFn((action, ...args) => {
      try {
        // 跳转
        callbacks[action](...args);
      } catch (err) {
        logger.error('Catch error in onSuccess', {
          action,
          args,
          err,
        });
      }
    });

    const {
      federatedMaster,
      federatedEnabled,
      master: masterSite,
    } = useFederated({
      locale: currentLocale,
      decrypt,
      login: loginWallet,
      handleLoginResult,
      handleRefreshUser,
      setRefreshToken,
      setSessionToken,
    });

    const { checkMatch } = useProtectedRoutes({ protectedRoutes: props.protectedRoutes || [] });

    const latestUserSessions = useLatest(userSessions);

    const getUserSessions = useMemoizedFn(async () => {
      if (!loadedUserSessions) {
        await refreshUserSessions();
        await sleep(100);
      }
      return latestUserSessions.current;
    });

    const closeConnect = useMemoizedFn(() => {
      state.open = false;
      connectApi.close();
    });

    /**
     * 打开 did-connect
     * @param {object} openConnectOptions
     * @param {boolean} openConnectOptions.hideCloseButton - 是否隐藏关闭按钮
     * @param {function} openConnectOptions.onSuccess - 成功回调
     */
    const openConnect = async (openConnectOptions = {}) => {
      let localeAction = state.action;
      if (pageState?.options?.origin === 'switch-did') {
        localeAction = pageState?.options?.origin;
      }
      const messages = translations[localeAction];

      const connectMessage = messages[currentLocale] || messages.en;
      const extraParams = {
        ...opts.extraParams,
        ...props.extraParams,
        ...pageState.extraParams,
        previousUserDid: state?.user?.did,
        email: state?.user?.email,
      };

      if (localeAction === 'switch-did' && extraParams?.forceConnected) {
        const t = (key, data = {}) => {
          return translate(translations['switch-specified-did'], key, currentLocale, 'en', data);
        };
        connectMessage.title = t('title');
        connectMessage.scan = t('scan', { did: extraParams.forceConnected });
        connectMessage.confirm = t('confirm');
        connectMessage.success = t('success');
      }
      // HACK: 部分参数不用传递给 server 端，需要手动移除

      delete extraParams?.openMode;
      const params = {
        action: state.action,
        locale: currentLocale,
        hideCloseButton: openConnectOptions?.hideCloseButton,
        extraParams,
        options: { ...(props.options || {}), ...pageState.options },
        checkTimeout: props.timeout,
        webWalletUrl: props.webWalletUrl,
        messages: connectMessage,
        useSocket: props.useSocket,
        ...omit(props, [
          'action',
          'locale',
          // NOTICE: 将 serviceHost 也透传给 DID Connect 组件
          // 'serviceHost',
          'appendAuthServicePrefix',
          'autoDisconnect',
          'children',
          'timeout',
          'extraParams',
          'options',
          'webWalletUrl',
          'messages',
          'useSocket',
          'lazyRefreshToken',
          'apiOptions',
          'protectedRoutes',
        ]),
        // 允许 login/switchProfile/switchDid 的时候传入自定义参数
        ...pick(pageState.extraParams || {}, [
          'autoConnect',
          'forceConnected',
          'saveConnect',
          'useSocket',
          'passkeyBehavior',
        ]),
        // baseUrl: state.baseUrl || props.baseUrl || '',

        // 注意 prefix 经过了特殊处理, 优先级高于 "...rest", 所以放在其后
        prefix: pageState.prefix,
        allowWallet: !isUndefined(pageState.allowWallet)
          ? pageState.allowWallet
          : state.action !== 'login' ||
            ['1', 'true', undefined, null].includes(globalThis.blocklet?.DID_CONNECT_ALLOW_WALLET),
        onSuccess: async (...args) => {
          onSuccess(state.action, ...args);
          if (isFunction(openConnectOptions?.onSuccess)) {
            await openConnectOptions.onSuccess(...args);
          }
          state.open = false;
        },
        onClose(...args) {
          onClose(state.action, ...args);
        },
        onError(err) {
          console.error(err);
        },
      };
      state.open = true;

      if (
        ['login'].includes(state.action) &&
        pageState.openMode === 'window' &&
        !(browser.wallet || browser.arcSphere)
      ) {
        try {
          const mergeParams = {
            ...params,
            baseUrl: window.location.origin,
          };
          // HACK: 在某些情况下，prefix 中已经携带了域名，这时不能再拼接一次
          if (!isUrl(pageState.prefix)) {
            mergeParams.prefix = joinURL(window.location.origin, props.serviceHost, pageState.prefix);
          }
          if (!mergeParams.extraParams?.provider) {
            mergeParams.extraParams.provider = state?.user?.provider || 'wallet';
          }
          await connectApi.openPopup(mergeParams, { locale: currentLocale });
        } catch (err) {
          if (err instanceof NotOpenError) {
            closeConnect();
          }
          if (err.message === 'Timeout') {
            Toast.error(didConnectTimeoutError[currentLocale] || didConnectTimeoutError.en);
          } else {
            console.error(err);
          }
        }
      } else {
        connectApi.open(params);
      }
    };

    const autoSwitchDid = useMemoizedFn(async () => {
      // NOTICE: 此处获得的 trimedUrl 是当前 location.href 精简参数后的 url，无需进行安全处理
      const { params: decodedParams, url: trimedUrl } = await decodeUrlParams();
      if (decodedParams) {
        const { switchBehavior, forceConnected, sourceAppPid, showClose } = decodedParams;

        if (forceConnected === state.user.did) {
          window.history.replaceState(null, 'trim', trimedUrl);
        } else if (switchBehavior === 'required') {
          switchDid(() => {}, {
            openMode: 'redirect',
            redirect: trimedUrl,
            sourceAppPid,
            forceConnected,
          });
        } else if (switchBehavior === 'disabled') {
          window.history.replaceState(null, 'trim', trimedUrl);
        } else {
          confirmApi.open({
            title: translations.switchAccountDialog[currentLocale].title,
            content: (
              <>
                <Typography variant="body1" sx={{ textAlign: 'center', mb: 2 }}>
                  {translations.switchAccountDialog[currentLocale].description}
                </Typography>
                <Box sx={{ display: 'flex', alignItems: 'center', color: 'grey.A700' }}>
                  <Box
                    sx={{
                      display: 'flex',
                      flexDirection: 'column',
                      alignItems: 'center',
                      gap: 1.5,
                      lineHeight: 1,
                    }}>
                    <Avatar did={state.user.did} shape="circle" variant="circle" size={80} />
                    <DID did={state.user.did} showAvatar={false} compact responsive={false} />
                    {translations.switchAccountDialog[currentLocale].currentAccount}
                  </Box>
                  <Box sx={{ display: 'flex', justifyContent: 'center', px: 2 }}>
                    <Box
                      component={Icon}
                      icon={KeyboardDoubleArrowRightRoundedIcon}
                      sx={{
                        fontSize: 24,
                        color: palette.grey[400],
                        animation: 'right 2.5s linear infinite',

                        '@keyframes right': {
                          '0%': {
                            transform: 'translateX(-3px)',
                            opacity: 0,
                          },
                          '60%': {
                            transform: 'translateX(3px)',
                            opacity: 1,
                          },
                          '100%': {
                            transform: 'translateX(0px)',
                            opacity: 0,
                          },
                        },
                      }}
                    />
                  </Box>
                  <Box
                    sx={{
                      display: 'flex',
                      flexDirection: 'column',
                      alignItems: 'center',
                      gap: 1.5,
                      lineHeight: 1,
                      fontWeight: 500,
                    }}>
                    <Avatar did={forceConnected} shape="circle" variant="circle" size={80} />
                    <DID did={forceConnected} showAvatar={false} compact responsive={false} />
                    {translations.switchAccountDialog[currentLocale].nextAccount}
                  </Box>
                </Box>
              </>
            ),
            showCloseButton: showClose,
            showCancelButton: false,
            confirmButtonText: translations.switchAccountDialog[currentLocale].confirm,
            async onConfirm(close) {
              const userSessionList = await getUserSessions();
              const findUserSession = userSessionList.find((x) => x.userDid === forceConnected);

              if (findUserSession) {
                loginUserSession(findUserSession).then((result) => {
                  updateConnectedInfo(
                    {
                      connected_did: findUserSession.user.did,
                      connected_pk: findUserSession.user.pk,
                      connected_wallet_os: findUserSession.extra.walletOS,
                      connected_app: getAppId(),
                    },
                    true
                  );
                  handleLoginResult({ ...result, encrypted: false });
                  handleRefreshUser({ showProgress: true }).then(async () => {
                    await sleep(200);
                    events.emit(EVENTS.LOGIN, result, decrypt, session.current);
                    notifyBridge(session.current);
                    await connectToDidSpaceForFullAccess();
                  });
                });
                close();
                return;
              }
              switchDid(
                () => {
                  close();
                  window.location.replace(trimedUrl);
                },
                { sourceAppPid, forceConnected, enableSwitchApp: false },
                { showQuickConnect: true }
              );
            },
          });
        }
      }
    });

    /**
     * @type {import('../types').OpenDidConnect}
     */
    const openDidConnect = useMemoizedFn(async (params, options) => {
      const requirements = Object.assign(
        {
          login: true,
          bindWallet: true,
          bindDidSpaces: false,
        },
        options?.requirements
      );
      const openMode = options?.openMode || 'popup';
      if (requirements.login) {
        await ensureLogin();
      }
      if (requirements.bindWallet) {
        await new Promise((resolve) => {
          bindWallet(() => {
            resolve();
          });
        });
      }
      if (requirements.bindDidSpaces) {
        if (!(await didSpacesIsRequired(state?.user))) {
          await new Promise((resolve) => {
            if (requirements.bindDidSpaces === 'full') {
              connectToDidSpaceForFullAccess({
                onSuccess() {
                  resolve();
                },
              });
            } else if (requirements.bindDidSpaces === 'read') {
              connectToDidSpaceForImport({
                onSuccess() {
                  resolve();
                },
              });
            }
          });
        }
      }

      if (openMode === 'window') {
        connectApi.openPopup(params, options);
      } else if (openMode === 'popup') {
        connectApi.open(params);
      }
    });

    useUpdateEffect(() => {
      const count = Number(state.unReadCount || 0); // 避免 unReadCount 为 string 类型
      setUnReadCount(count);
    }, [state.unReadCount]);

    const removeMagicToken = useMemoizedFn(() => {
      if (searchParams.get('magicToken')) {
        searchParams.delete('magicToken');
      }
      if (searchParams.toString()) {
        window.history.replaceState({}, '', `${window.location.pathname}?${searchParams.toString()}`);
      }
    });

    const verifyMagicToken = useMemoizedFn((magicToken) => {
      return new Promise((resolve, reject) => {
        connectApi.open({
          action: 'login',
          locale: currentLocale,
          prefix: pageState.prefix,
          useSocket: false,
          messages: {
            title: currentLocale === 'zh' ? '验证 MagicLink Token' : 'Verify MagicLink Token',
          },
          magicToken,
          onSuccess(...args) {
            onLogin(...args);
            removeMagicToken();
            resolve();
          },
          onError(err) {
            console.error(err);
            reject(err);
          },
        });
      });
    });

    const currentSession = {
      ...state,
      loading: pageState.autoConnect ? !state.user || state.loading : state.loading,
      openDidConnect,
      login,
      logout,
      switchDid,
      autoSwitchDid,
      refreshProfile,
      switchProfile,
      switchPassport,
      connectToDidSpaceForImport,
      connectToDidSpaceForFullAccess,
      bindWallet,
      refresh: handleRefreshUser,
      updateConnectedInfo,
      // federated relates
      federatedMaster,
      // oauth relates
      useOAuth,
      OAuthProvider,
      OAuthConsumer,
      OAuthContext,
      // passkey relates
      usePasskey,
      PasskeyProvider,
      PasskeyConsumer,
      PasskeyContext,
      // user related
      useDid,
      WrapDid,
      getUserSessions,
      async loginUserSession(userSessionItem) {
        const loginResult = await loginUserSession(userSessionItem);
        updateConnectedInfo(
          {
            connected_did: userSessionItem.user.did,
            connected_pk: userSessionItem.user.pk,
            connected_wallet_os: userSessionItem.extra.walletOS,
            connected_app: getAppId(),
          },
          true
        );
        handleLoginResult({ ...loginResult, encrypted: false });
        await handleRefreshUser({ showProgress: true });
        await sleep(200);
        events.emit(EVENTS.LOGIN, loginResult, decrypt, session.current);
        notifyBridge(session.current);
        await connectToDidSpaceForFullAccess();
      },
      unReadCount,
      setUnReadCount,

      verifyMagicToken,
      // 二次认证相关方法
      withSecondaryAuth,
    };

    const session = useLatest(currentSession);

    const sessionValue = {
      api: service,
      events,
      storage: sessionTokenStorage,
      connectApi,
      session: session.current,
    };

    const callbacks = {
      login: onLogin,
      'switch-profile': onSwitchProfile,
      'switch-passport': onSwitchPassport,
      'bind-wallet': onBindWallet,
    };

    // user change 事件
    useUpdateEffect(() => {
      if (state.initialized) {
        const isFirst = !prevInitialized;
        events.emit('change', { user: cloneDeep(state.user), isFirst });
        if (state.user) {
          if (globalThis.blocklet) {
            const data = {
              did: state.user.did,
              host: window.location.host,
              appPid: globalThis.blocklet.appPid,
              visitorId: getVisitorId(),
              sourceAppPid: state.user.sourceAppPid,
              fullName: state.user.fullName,
            };
            debug('bridge callArc: onChange', {
              isFirst,
              user: data,
            });
            bridge.callArc('onChange', {
              ...data,
              user: data,
              isFirst,
            });
          }
        }
      }
    }, [state.user, state.initialized, events]);

    // 检查当前登录的用户与 url 中参数指定的用户是否一致
    useUpdateEffect(() => {
      if (state.initialized && state.user) {
        autoSwitchDid();
      }
    }, [state.initialized]);

    useMount(async () => {
      const bridgeOnLogin = (result) => {
        if (isNil(result.encrypted)) {
          result.encrypted = false;
        }
        if (window.temporaryDIDConnectOnSuccess instanceof Function) {
          debug('bridgeOnLogin: temporaryDIDConnectOnSuccess', { result });
          window.temporaryDIDConnectOnSuccess(result, (v) => v);
        } else {
          debug('bridgeOnLogin: onLogin', { result });
          onLogin(result);
        }
      };
      bridge.registerBlocklet('callLoginOAuth', (options) => {
        debug('bridge registerBlocklet: callLoginOAuth', options);
        connectApi.loginOAuth({
          ...options,
          onLogin: bridgeOnLogin,
        });
      });
      bridge.registerBlocklet('logout', logout);
      // NOTICE: login 是指 arcsphere 已经完成登录，传回的 result 是登录后的数据
      bridge.registerBlocklet('login', (result) => {
        debug('bridge registerBlocklet: login', result);
        bridgeOnLogin(result);
      });

      // 确保 普通浏览器中 visitorId 存在
      ensureVisitorId();
      const visitorId = getVisitorId();

      if (!visitorId) {
        // 确保 ArcSphere 中 visitorId 存在
        if (browser.arcSphere) {
          debug('bridge callArc: getVisitorId');
          const walletVisitorId = await bridge.callArc('getVisitorId');
          debug('bridge callArc: getVisitorId result', { walletVisitorId });
          setVisitorId(walletVisitorId);
        } else if (browser.wallet) {
          const walletVisitorId = await getMobileVisitorId();
          setVisitorId(walletVisitorId);
        }
      }

      // ensure we are in the same app
      const connectedApp = Cookie.get('connected_app');
      const actualApp = getAppId();

      if (props.autoDisconnect && connectedApp && actualApp && connectedApp !== actualApp) {
        clearSession();
        state.initialized = true;
        if (pageState.autoConnect) {
          openConnect();
        } else {
          closeConnect();
        }
        return;
      }

      const sessionToken = getSessionToken();

      // HACK: 如果页面处于 iframe 中，将不进行自动登录的逻辑，防止页面会产生两次钱包的自动登录流程
      if (window?.self === window?.parent) {
        let platform = 'web';
        if (browser.mobile.apple.device) {
          platform = 'ios';
        } else if (browser.mobile.android.device) {
          platform = 'android';
        }
        if (browser.wallet && checkEnableAutoLogin({ version: browser.walletVersion, platform })) {
          if (!sessionToken) {
            try {
              state.loading = true;
              const result = await loginInMobile();
              if (result) {
                await onLogin({
                  sessionToken: result.sessionToken,
                  refreshToken: result.refreshToken,
                  visitorId: result.visitorId,
                  encrypted: false,
                });
                state.initialized = true;
                return;
              }
            } finally {
              state.loading = false;
            }
          }
        }
      }

      const magicToken = searchParams.get('magicToken');
      if (magicToken) {
        try {
          state.initialized = true;
          await verifyMagicToken(magicToken);
        } catch (err) {
          console.error('verifyMagicToken failed', err);
        }
      }

      if (sessionToken) {
        await handleRefreshUser({ showProgress: true });
        state.initialized = true;

        if (state.user) {
          await connectToDidSpaceForFullAccess();
        }
        return;
      }

      if (typeof window !== 'undefined') {
        // If a login token exist in url, set that token in storage
        const url = new URL(window.location.href);
        const loginToken = url.searchParams.get('loginToken');
        if (loginToken) {
          handleLoginResult({ loginToken, encrypted: false });

          url.searchParams.delete('loginToken');
          window.history.replaceState({}, window.title, url.href);
          return;
        }
      }

      // If a refresh token exist and session token NOT exist, do refresh session
      const refreshToken = getRefreshToken();
      if (refreshToken) {
        await handleRefreshToken(true);
        state.initialized = true;
        return;
      }

      try {
        // await tryAutoLoginFederated();
      } catch (err) {
        console.error('Federated: tryAutoLogin failed', err);
      } finally {
        state.initialized = true;
      }

      if (pageState.autoConnect) {
        openConnect();
      } else {
        closeConnect();
      }

      if (
        browser.arcSphere &&
        ![
          // 不应该唤起自动登录的路由
          joinURL(BLOCKLET_SERVICE_PATH_PREFIX, 'login'),
          joinURL(BLOCKLET_SERVICE_PATH_PREFIX, 'connect'),
          joinURL(BLOCKLET_SERVICE_PATH_PREFIX, 'lost-passport'),
        ].includes(window.location.pathname)
      ) {
        if (!sessionToken) {
          try {
            debug('bridge callArc: autoLogin');
            const result = await bridge.callArc('autoLogin');
            debug('bridgecallArc: autoLogin result', result);
            if (result) {
              await onLogin({
                sessionToken: result.sessionToken,
                refreshToken: result.refreshToken,
                visitorId: result.visitorId,
                encrypted: false,
              });
            }
          } catch (err) {
            logger.error('Failed to autoLogin ArcSphere', err);
          }
        }
      }
    });
    const seamless = searchParams.has('popup');

    if (checkMatch()) {
      if (!state.initialized) {
        return <Center>{seamless ? null : <CircularProgress />}</Center>;
      }
    }

    const providerProps = {
      locale: currentLocale,
      onSwitchPassport: ({ sessionToken, refreshToken }) =>
        onSwitchPassport({ sessionToken, refreshToken, encrypted: false }),
      onUnbindOAuth: () => handleRefreshUser(),
      onBindOAuth: () => handleRefreshUser(),
      onAddPasskey: () => handleRefreshUser(),
      onRemovePasskey: () => handleRefreshUser(),
    };

    return (
      <Provider value={sessionValue}>
        <PasskeyProvider {...providerProps}>
          <OAuthProvider {...providerProps}>
            {!state.open && typeof props.children === 'function' ? props.children(state) : props.children}
            {connectHolder}
            {confirmHolder}
            {DidSpacesGuideView}
            {opts.rolling && <WindowFocusAware callback={() => renewToken()} />}
            <WindowFocusAware callback={syncSessionSate} />
          </OAuthProvider>
        </PasskeyProvider>
      </Provider>
    );
  }

  SessionProvider.propTypes = {
    children: PropTypes.any.isRequired,
    serviceHost: PropTypes.string,
    action: PropTypes.string,
    prefix: PropTypes.string,
    appendAuthServicePrefix: PropTypes.bool,
    locale: PropTypes.string,
    timeout: PropTypes.number,
    autoConnect: PropTypes.bool, // should we open connect dialog when session not found
    autoDisconnect: PropTypes.bool, // should we auto disconnect on appId mismatch
    useSocket: PropTypes.bool, // should we auto disconnect on appId mismatch
    extraParams: PropTypes.object,
    options: PropTypes.object,
    webWalletUrl: PropTypes.string,
    protectedRoutes: PropTypes.arrayOf(PropTypes.string),
    apiOptions: PropTypes.object,
    lazyRefreshToken: PropTypes.bool,
  };

  function withSession(Component) {
    return function WithSessionComponent(props) {
      return (
        <ToastProvider>
          <Consumer>{(sessionProps) => <Component {...props} {...sessionProps} />}</Consumer>
        </ToastProvider>
      );
    };
  }

  return {
    SessionProvider: (props) => {
      return (
        <ToastProvider>
          <FederatedProvider>
            <SessionProvider {...props} />
          </FederatedProvider>
        </ToastProvider>
      );
    },
    SessionConsumer: Consumer,
    SessionContext,
    withSession,
  };
}

export function createAuthServiceSessionContext({ storageEngine = 'cookie' } = {}) {
  const storageKey = SESSION_TOKEN_STORAGE_KEY;
  const refreshTokenStorageKey = REFRESH_TOKEN_STORAGE_KEY;

  // componentId
  let componentId = null;
  if (typeof window !== 'undefined') {
    const blocklet = globalThis.blocklet || {};

    // FIXME: @zhanghan 当 connect 组件使用不同的 baseUrl 时，不应该直接从 window.blocklet 去取值
    componentId = blocklet.componentId;
  }

  if (storageEngine === 'cookie') {
    let path = '/';
    // FIXME: @zhanghan sessionToken 也应该从 jwt 中读取过期时间，而不是通过配置来判断？
    let sessionTokenExpireInDays = 1;

    if (typeof window !== 'undefined') {
      const env = window.env || {};
      const blocklet = window.blocklet || {};

      // FIXME: @zhanghan 当 connect 组件使用不同的 baseUrl 时，不应该直接从 window.blocklet 去取值
      path = env.groupPathPrefix || blocklet.groupPrefix || blocklet.prefix || '';
      path = path.replace(/\/+$/, '');
      path = path || '/';

      // for backward compatibility
      // 不支持 refreshToken 的旧版 server 不会在 window.blocklet 中返回 serverVersion
      if (blocklet.serverVersion) {
        sessionTokenExpireInDays = formatCacheTtl(window.blocklet?.settings?.session?.cacheTtl, 1 / 24); // 1h
      }
    }

    return createSessionContext(
      storageKey,
      'cookie',
      {
        path,
        returnDomain: false,
        expireInDays: sessionTokenExpireInDays,
      },
      {
        appendAuthServicePrefix: true,
        extraParams: { componentId },
        refreshTokenStorageKey,
      }
    );
  }

  if (storageEngine === 'localStorage') {
    return createSessionContext(storageKey, 'ls', {}, true, {
      appendAuthServicePrefix: true,
      extraParams: { componentId },
    });
  }

  throw new Error('storageEngine must be cookie or localStorage');
}

export { SessionContext };
