cyb/src/redux/features/pocket.ts

import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { Dispatch } from 'redux';
import { localStorageKeys } from 'src/constants/localStorageKeys';
import { Account, Accounts, AccountValue, DefaultAccount } from 'src/types/defaultAccount';
import { POCKET } from '../../utils/config';
import { removeEncryptedMnemonic, removeAllMnemonics } from '../../utils/utils';
import { RootState } from '../store';

type SliceState = {
  actionBar: {
    tweet: string;
  };
  defaultAccount: DefaultAccount;
  accounts: null | Accounts;
  isInitialized: boolean;
};

const initialState: SliceState = {
  actionBar: {
    tweet: POCKET.STAGE_TWEET_ACTION_BAR.TWEET, // stage for tweet ActionBar: 'addAvatar' 'follow' 'tweet'
  },
  isInitialized: false,
  defaultAccount: {
    name: null,
    account: null,
  },
  accounts: null,
};

const checkAddress = (obj, network, address) =>
  Object.keys(obj).some((k) => {
    if (obj[k][network]) {
      return obj[k][network].bech32 === address;
    }
  });

function saveToLocalStorage(state: SliceState) {
  const { defaultAccount, accounts } = state;

  defaultAccount &&
    localStorage.setItem(
      localStorageKeys.pocket.POCKET,
      JSON.stringify({
        [defaultAccount.name]: defaultAccount.account,
      })
    );
  accounts &&
    localStorage.setItem(localStorageKeys.pocket.POCKET_ACCOUNT, JSON.stringify(accounts));
}

const slice = createSlice({
  name: 'pocket',
  initialState,
  reducers: {
    setDefaultAccount: (
      state,
      { payload: { name, account } }: PayloadAction<{ name: string; account?: Account }>
    ) => {
      state.defaultAccount = {
        name,
        account: account || state.accounts?.[name] || null,
      };

      saveToLocalStorage(state);
    },
    setAccounts: (state, { payload }: PayloadAction<Accounts>) => {
      state.accounts = payload;

      saveToLocalStorage(state);
    },
    setInitialized: (state) => {
      state.isInitialized = true;
    },
    setStageTweetActionBar: (state, { payload }: PayloadAction<string>) => {
      state.actionBar.tweet = payload;
    },

    // bullshit
    deleteAddress: (state, { payload }: PayloadAction<string>) => {
      if (state.accounts) {
        Object.keys(state.accounts).forEach((accountKey) => {
          Object.keys(state.accounts[accountKey]).forEach((networkKey) => {
            if (state.accounts[accountKey][networkKey].bech32 === payload) {
              // Clean up mnemonic from localStorage if this was a wallet account
              if (state.accounts[accountKey][networkKey].keys === 'wallet') {
                removeEncryptedMnemonic(payload);
                // Fire lock event so signerClient drops the active signer
                window.dispatchEvent(new CustomEvent('__cyb_wallet_locked'));
              }
              delete state.accounts[accountKey][networkKey];

              if (Object.keys(state.accounts[accountKey]).length === 0) {
                delete state.accounts[accountKey];
              }

              if (state.defaultAccount?.account?.cyber?.bech32 === payload) {
                const entries = Object.entries(state.accounts);

                const entryCyber = entries.find(([, value]) => value.cyber?.bech32);

                if (entryCyber) {
                  state.defaultAccount = {
                    name: entryCyber[0],
                    account: entryCyber[1],
                  };
                } else {
                  state.defaultAccount = {
                    name: null,
                    account: null,
                  };
                }
              }

              saveToLocalStorage(state);
            }
          });
        });
      }
    },
  },
});

export const selectCurrentAddress = (store: RootState) =>
  store.pocket.defaultAccount.account?.cyber?.bech32;

export const { setDefaultAccount, setAccounts, setStageTweetActionBar, deleteAddress } =
  slice.actions;

export default slice.reducer;

// refactor this
// Migrate legacy 'keplr' accounts to 'read-only' (one-time, idempotent)
function migrateKeplrAccounts() {
  try {
    const raw = localStorage.getItem(localStorageKeys.pocket.POCKET_ACCOUNT);
    if (!raw) return;

    let changed = false;
    const accounts: Accounts = JSON.parse(raw);

    Object.keys(accounts).forEach((name) => {
      Object.keys(accounts[name]).forEach((network) => {
        if (accounts[name][network].keys === 'keplr') {
          accounts[name][network].keys = 'read-only';
          changed = true;
        }
      });
    });

    if (changed) {
      localStorage.setItem(localStorageKeys.pocket.POCKET_ACCOUNT, JSON.stringify(accounts));

      // Also patch the default account pocket entry
      const pocketRaw = localStorage.getItem(localStorageKeys.pocket.POCKET);
      if (pocketRaw) {
        const pocket = JSON.parse(pocketRaw);
        Object.keys(pocket).forEach((name) => {
          if (pocket[name]) {
            Object.keys(pocket[name]).forEach((network) => {
              if (pocket[name][network]?.keys === 'keplr') {
                pocket[name][network].keys = 'read-only';
              }
            });
          }
        });
        localStorage.setItem(localStorageKeys.pocket.POCKET, JSON.stringify(pocket));
      }

      // Flag for one-time adviser notification
      if (!localStorage.getItem('cyb:keplr-migrated')) {
        localStorage.setItem('cyb:keplr-migrated', '1');
      }
    }
  } catch (e) {
    console.warn('keplr account migration skipped due to corrupt localStorage:', e);
  }
}

export const initPocket = () => (dispatch: Dispatch) => {
  // Migrate keplr accounts before loading
  migrateKeplrAccounts();

  let defaultAccounts = null;
  let defaultAccountsKeys = null;
  let accountsTemp: Accounts | null = null;

  const localStoragePocketAccount = localStorage.getItem(localStorageKeys.pocket.POCKET_ACCOUNT);
  const localStoragePocket = localStorage.getItem(localStorageKeys.pocket.POCKET);
  if (localStoragePocket !== null) {
    const localStoragePocketData = JSON.parse(localStoragePocket);
    const keyPocket = Object.keys(localStoragePocketData)[0];
    const accountPocket = Object.values(localStoragePocketData)[0];
    defaultAccounts = accountPocket;
    defaultAccountsKeys = keyPocket;
  }
  if (localStoragePocketAccount !== null) {
    const localStoragePocketAccountData = JSON.parse(localStoragePocketAccount);
    if (localStoragePocket === null) {
      const keys0 = Object.keys(localStoragePocketAccountData)[0];
      localStorage.setItem(
        localStorageKeys.pocket.POCKET,
        JSON.stringify({ [keys0]: localStoragePocketAccountData[keys0] })
      );
      defaultAccounts = localStoragePocketAccountData[keys0];
      defaultAccountsKeys = keys0;
    } else if (defaultAccountsKeys !== null) {
      accountsTemp = {
        [defaultAccountsKeys]: localStoragePocketAccountData[defaultAccountsKeys] || undefined,
        ...localStoragePocketAccountData,
      };
    }
  } else {
    localStorage.removeItem(localStorageKeys.pocket.POCKET);
    localStorage.removeItem(localStorageKeys.pocket.POCKET_ACCOUNT);
  }

  defaultAccountsKeys &&
    defaultAccounts &&
    dispatch(
      setDefaultAccount({
        name: defaultAccountsKeys,
        account: defaultAccounts,
      })
    );

  accountsTemp &&
    Object.keys(accountsTemp).forEach((key) => {
      if (!accountsTemp[key] || Object.keys(accountsTemp[key]).length === 0) {
        delete accountsTemp[key];
      }
    });

  accountsTemp && dispatch(setAccounts(accountsTemp));

  dispatch(slice.actions.setInitialized());
};

const defaultNameAccount = () => {
  let key = 'Account 1';
  let count = 1;

  const localStorageCount = localStorage.getItem('count');

  if (localStorageCount !== null) {
    const dataCount = JSON.parse(localStorageCount);
    count = parseFloat(dataCount);
    key = `Account ${count}`;
  }

  localStorage.setItem('count', JSON.stringify(count + 1));

  return key;
};

export const addAddressPocket = (accounts: AccountValue) => (dispatch: Dispatch) => {
  const key = accounts.name || defaultNameAccount();

  let dataPocketAccount = null;
  let valueObj = {};
  let pocketAccount: Accounts = {};

  const localStorageStory = localStorage.getItem(localStorageKeys.pocket.POCKET_ACCOUNT);

  if (localStorageStory !== null) {
    dataPocketAccount = JSON.parse(localStorageStory);
    valueObj = Object.values(dataPocketAccount);
  }

  const isAdded = !checkAddress(valueObj, 'cyber', accounts.bech32);

  if (!isAdded) {
    return;
  }

  const cyberAccounts: Account = {
    cyber: accounts,
  };

  if (localStorageStory !== null) {
    pocketAccount = { [key]: cyberAccounts, ...dataPocketAccount };
  } else {
    pocketAccount = { [key]: cyberAccounts };
  }

  if (Object.keys(pocketAccount).length > 0) {
    dispatch(setAccounts(pocketAccount));
    if (accounts.keys !== 'read-only') {
      const currentPocket = localStorage.getItem(localStorageKeys.pocket.POCKET);
      if (!currentPocket) {
        dispatch(setDefaultAccount({ name: key, account: cyberAccounts }));
      }
    }
  }
};

Synonyms

pussy-ts/src/redux/features/pocket.ts

Neighbours