Add persistence of throwaway accounts (#315)

* add account membership status

* add storage and account listeners

* update membership

* update ui

* update membership status value

* use boolean instead of undefined

* rename callback function

* remove noise

* listeners -> callbacks

* update storage

* lint issue workaround

* update account storing
This commit is contained in:
Pavel 2022-10-07 20:05:09 +02:00 committed by GitHub
parent ca0d508f7f
commit c39b84a1e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 284 additions and 237 deletions

View File

@ -5,8 +5,11 @@ import { expect, test } from 'vitest'
import { Account } from './account'
import type { Client } from './client'
test('should verify the signature', async () => {
const account = new Account()
// @fixme
const account = new Account({} as unknown as Client)
const message = utf8ToBytes('123')
const messageHash = keccak256(message)
@ -20,7 +23,8 @@ test('should verify the signature', async () => {
})
test('should not verify signature with different message', async () => {
const account = new Account()
// @fixme
const account = new Account({} as unknown as Client)
const message = utf8ToBytes('123')
const messageHash = keccak256(message)

View File

@ -1,18 +1,34 @@
import { keccak256 } from 'ethereum-cryptography/keccak'
import { getPublicKey, sign, utils } from 'ethereum-cryptography/secp256k1'
import { bytesToHex, concatBytes } from 'ethereum-cryptography/utils'
import {
bytesToHex,
concatBytes,
hexToBytes,
} from 'ethereum-cryptography/utils'
import { compressPublicKey } from '../utils/compress-public-key'
import { generateUsername } from '../utils/generate-username'
export class Account {
public privateKey: string
public publicKey: string
public chatKey: string
public username: string
import type { Client } from './client'
import type { Community } from './community/community'
constructor() {
const privateKey = utils.randomPrivateKey()
type MembershipStatus = 'none' | 'requested' | 'approved' | 'kicked' // TODO: add 'banned'
export class Account {
#client: Client
privateKey: string
publicKey: string
chatKey: string
username: string
membership: MembershipStatus = 'none'
constructor(client: Client, initialPrivateKey?: string) {
this.#client = client
const privateKey = initialPrivateKey
? hexToBytes(initialPrivateKey)
: utils.randomPrivateKey()
const publicKey = getPublicKey(privateKey)
this.privateKey = bytesToHex(privateKey)
@ -22,7 +38,7 @@ export class Account {
}
// sig must be a 65-byte compact ECDSA signature containing the recovery id as the last element.
sign = async (payload: Uint8Array) => {
async sign(payload: Uint8Array) {
const hash = keccak256(payload)
const [signature, recoverId] = await sign(hash, this.privateKey, {
recovered: true,
@ -31,4 +47,34 @@ export class Account {
return concatBytes(signature, new Uint8Array([recoverId]))
}
updateMembership(community: Community): void {
const isMember = community.isMember('0x' + this.publicKey)
switch (this.membership) {
case 'none': {
community.requestToJoin()
this.membership = 'requested'
// fixme: this is a hack to make sure the UI updates when the membership status changes
this.#client.account = this
return
}
case 'approved': {
if (isMember === false) {
this.membership = 'kicked'
this.#client.account = this // fixme
}
return
}
case 'requested': {
if (isMember) {
this.membership = 'approved'
this.#client.account = this // fixme
}
return
}
}
}
}

View File

@ -14,12 +14,24 @@ import { Account } from './account'
import { ActivityCenter } from './activityCenter'
import { Community } from './community/community'
import { handleWakuMessage } from './community/handle-waku-message'
import { LocalStorage } from './storage'
import type { Storage } from './storage'
import type { Waku } from 'js-waku'
const THROWAWAY_ACCOUNT_STORAGE_KEY = 'throwaway_account'
export interface ClientOptions {
/**
* Public key of a community to join.
*/
publicKey: string
environment?: 'production' | 'test'
/**
* Custom storage for data persistance
* @default window.localStorage
*/
storage?: Storage
}
class Client {
@ -40,9 +52,13 @@ class Client {
#wakuDisconnectionTimer: ReturnType<typeof setInterval>
public activityCenter: ActivityCenter
public account?: Account
public community: Community
#account?: Account
#accountCallbacks = new Set<(account?: Account) => void>()
storage: Storage
constructor(
waku: Waku,
wakuDisconnectionTimer: ReturnType<typeof setInterval>,
@ -53,11 +69,22 @@ class Client {
this.wakuMessages = new Set()
this.#wakuDisconnectionTimer = wakuDisconnectionTimer
// Storage
this.storage = options.storage ?? new LocalStorage()
// Activity Center
this.activityCenter = new ActivityCenter(this)
// Community
this.community = new Community(this, options.publicKey)
// Restore account if exists
const privateKey = this.storage.getItem<string>(
THROWAWAY_ACCOUNT_STORAGE_KEY
)
if (privateKey) {
this.#account = new Account(this, privateKey)
}
}
static async start(options: ClientOptions) {
@ -110,18 +137,39 @@ class Client {
await this.waku.stop()
}
public createAccount = async (): Promise<Account> => {
this.account = new Account()
get account() {
return this.#account
}
await this.community.requestToJoin()
set account(account: Account | undefined) {
this.#account = account
this.storage.setItem(
THROWAWAY_ACCOUNT_STORAGE_KEY,
this.#account?.privateKey
)
for (const callback of this.#accountCallbacks) {
callback(this.#account)
}
}
public createAccount(): Account {
this.account = new Account(this)
this.account.updateMembership(this.community)
return this.account
}
// TODO?: should this exist
// public deleteAccount = () => {
// this.account = undefined
// }
public deleteAccount() {
this.account = undefined
}
public onAccountChange(listener: (account?: Account) => void) {
this.#accountCallbacks.add(listener)
return () => {
this.#accountCallbacks.delete(listener)
}
}
public sendWakuMessage = async (
type: keyof typeof ApplicationMetadataMessage.Type,
@ -133,11 +181,11 @@ class Client {
throw new Error('Waku not started')
}
if (!this.account) {
if (!this.#account) {
throw new Error('Account not created')
}
const signature = await this.account.sign(payload)
const signature = await this.#account.sign(payload)
const message = ApplicationMetadataMessage.encode({
type: type as ApplicationMetadataMessage.Type,
@ -146,7 +194,7 @@ class Client {
})
const wakuMesage = await WakuMessage.fromBytes(message, contentTopic, {
sigPrivKey: hexToBytes(this.account.privateKey),
sigPrivKey: hexToBytes(this.#account.privateKey),
symKey,
})
@ -154,7 +202,7 @@ class Client {
}
public handleWakuMessage = (wakuMessage: WakuMessage): void => {
handleWakuMessage(wakuMessage, this, this.community)
handleWakuMessage(wakuMessage, this, this.community, this.#account)
}
}

View File

@ -63,10 +63,8 @@ export class Community {
throw new Error('Failed to intiliaze Community')
}
this.description = description
// Observe community description
this.observe()
this.addMembers(this.description.members)
// Chats
await this.observeChatMessages(this.description.chats)
@ -235,6 +233,7 @@ export class Community {
// Community
// state
this.description = description
this.addMembers(this.description.members)
// callback
this.#callbacks.forEach(callback => callback({ ...this.description }))

View File

@ -17,6 +17,7 @@ import { recoverPublicKey } from '../../utils/recover-public-key'
import { getChatUuid } from './get-chat-uuid'
import { mapChatMessage } from './map-chat-message'
import type { Account } from '../account'
import type { Client } from '../client'
import type { Community } from './community'
import type { WakuMessage } from 'js-waku'
@ -25,7 +26,8 @@ export function handleWakuMessage(
wakuMessage: WakuMessage,
// state
client: Client,
community: Community
community: Community,
account?: Account
): void {
// decode (layers)
// validate
@ -97,6 +99,8 @@ export function handleWakuMessage(
community.handleDescription(decodedPayload)
community.setClock(BigInt(decodedPayload.clock))
account?.updateMembership(community)
break
}

View File

@ -0,0 +1,61 @@
type Unsubscribe = () => void
export interface Storage {
getItem: <T = string>(key: string) => T | null
setItem: (key: string, newValue: unknown) => void
removeItem: (key: string) => void
subscribe?: (key: string, callback: (value: unknown) => void) => Unsubscribe
}
export class LocalStorage implements Storage {
#prefix = 'status-im:'
#getStorageKey(key: string) {
return `${this.#prefix}${key}`
}
getItem<T>(key: string): T | null {
const value = window.localStorage.getItem(this.#getStorageKey(key))
if (!value) {
return null
}
try {
return JSON.parse(value) as T
} catch {
// invalid JSON
return null
}
}
setItem(key: string, value: unknown): boolean {
try {
window.localStorage.setItem(
this.#getStorageKey(key),
JSON.stringify(value)
)
return true
} catch {
return false
}
}
removeItem(key: string) {
return window.localStorage.removeItem(this.#getStorageKey(key))
}
subscribe(key: string, callback: (value: unknown) => void) {
const handler = (event: StorageEvent) => {
if (event.key === key && event.newValue) {
callback(JSON.parse(event.newValue))
}
}
window.addEventListener('storage', handler)
return () => {
window.removeEventListener('storage', handler)
}
}
}

View File

@ -4,12 +4,14 @@ import { expect, test } from 'vitest'
import { Account } from '../client/account'
import { recoverPublicKey } from './recover-public-key'
import type { Client } from '../client/client'
import type { ApplicationMetadataMessage } from '../protos/application-metadata-message'
test('should recover public key', async () => {
const payload = utf8ToBytes('hello')
const account = new Account()
// @fixme
const account = new Account({} as unknown as Client)
const signature = await account.sign(payload)
expect(bytesToHex(recoverPublicKey(signature, payload))).toEqual(
@ -60,7 +62,8 @@ test('should recover public key from fixture', async () => {
test('should not recover public key with different payload', async () => {
const payload = utf8ToBytes('1')
const account = new Account()
// @fixme
const account = new Account({} as unknown as Client)
const signature = await account.sign(payload)
const payload2 = utf8ToBytes('2')

View File

@ -1,15 +1,14 @@
import React from 'react'
import { useMembers, useProtocol } from '../../../../protocol'
import { useProtocol } from '../../../../protocol'
import { styled } from '../../../../styles/config'
import { Avatar, DialogTrigger, Text } from '../../../../system'
import { CommunityDialog } from './community-dialog'
export const CommunityInfo = () => {
const { community } = useProtocol()
const members = useMembers()
const { client } = useProtocol()
const { displayName, color } = community.identity!
const { displayName, color } = client.community.description.identity!
return (
<DialogTrigger>
@ -18,7 +17,7 @@ export const CommunityInfo = () => {
<div>
<Text>{displayName}</Text>
<Text color="gray" size={12}>
{members.length} members
{client.community.members.length} members
</Text>
</div>
</Button>

View File

@ -1,25 +1,14 @@
import React from 'react'
// import { CreateProfileDialog } from '../../../../components/create-profile-dialog'
// import { useLocalStorage } from '../../../../hooks/use-local-storage'
import { useAccount } from '../../../../protocol'
import { Button, Flex } from '../../../../system'
// import { DialogTrigger } from '../../../../system/dialog'
import { Grid } from '../../../../system/grid'
import { Heading } from '../../../../system/heading'
// import { ConnectWalletDialog } from './connect-wallet-dialog'
// import { SyncStatusProfileDialog } from './sync-status-profile-dialog'
// import { ThrowawayProfileFoundDialog } from './throwaway-profile-found-dialog'
export const GetStarted = () => {
// const [throwawayProfile] = useLocalStorage('cipherIdentityt', null)
const { account, createAccount } = useAccount()
// const handleSkip = () => {
// // TODO: Add skip logic
// }
const { createAccount } = useAccount()
const membershipRequested = account?.membership === 'requested'
return (
<Flex direction="column" align="center" gap={5} css={{ padding: '30px 0' }}>
@ -145,25 +134,11 @@ export const GetStarted = () => {
Want to jump into the discussion?
</Heading>
<Grid gap={3} align="center" justify="center">
{/* <DialogTrigger>
<Button>Sync with Status profile</Button>
<SyncStatusProfileDialog />
</DialogTrigger> */}
{/* <DialogTrigger>
<Button>Connect Wallet</Button>
<ConnectWalletDialog />
</DialogTrigger> */}
<Button onClick={createAccount}>Use Throwaway Profile</Button>
{/* <DialogTrigger>
<Button>Use Throwaway Profile</Button>
{account ? (
<ThrowawayProfileFoundDialog onSkip={handleSkip} />
) : (
<CreateProfileDialog />
)}
</DialogTrigger> */}
<Button onClick={createAccount} disabled={membershipRequested}>
{membershipRequested
? 'Membership Requested'
: 'Use Throwaway Profile'}
</Button>
</Grid>
</Flex>
)

View File

@ -1,55 +1,49 @@
import React from 'react'
import { useAccount } from '../../../../protocol'
import {
Avatar,
Dialog,
EmojiHash,
// EmojiHash,
EthAddress,
Flex,
Heading,
Text,
} from '../../../../system'
import type { Account } from '../../../../protocol'
interface Props {
onSkip: () => void
account: Account
}
export const ThrowawayProfileFoundDialog = (props: Props) => {
const { onSkip } = props
const { account } = props
const { account } = useAccount()
const handleLoadThrowawayProfile = () => {
const handleLoad = () => {
// TODO: load throwaway profile
}
if (!account) {
return null
}
return (
<Dialog title="Throwaway Profile Found">
<Dialog.Body gap="5">
<Flex direction="column" align="center" gap="2">
<Avatar size={64} />
<Avatar size={64} name={account.username} />
<Heading weight="600">{account.username}</Heading>
<Text color="gray">
Chatkey: 0x63FaC9201494f0bd17B9892B9fae4d52fe3BD377
</Text>
<EmojiHash />
<EthAddress color="gray">{account.chatKey}</EthAddress>
{/* <Text color="gray">Chatkey: {account.chatKey}</Text> */}
{/* <EmojiHash /> */}
</Flex>
<Text>
Throwaway profile is found in your local {"browser's"} storage.
Throwaway profile is found in your local storage.
<br />
Would you like to load it and use it?
Would you like to use it?
</Text>
</Dialog.Body>
<Dialog.Actions>
<Dialog.Action variant="outline" onClick={onSkip}>
Skip
</Dialog.Action>
<Dialog.Action onClick={handleLoadThrowawayProfile}>
<Dialog.Cancel variant="outline">Skip</Dialog.Cancel>
<Dialog.Action onClick={handleLoad}>
Load Throwaway Profile
</Dialog.Action>
</Dialog.Actions>

View File

@ -21,7 +21,7 @@ export const MainSidebar = () => {
<CommunityInfo />
<Chats />
{!account && (
{account?.membership !== 'approved' && (
<>
<Separator />
<GetStarted />

View File

@ -1,7 +1,15 @@
import React from 'react'
import { useAccount } from '../../protocol'
import { Avatar, Dialog, EmojiHash, Flex, Heading, Text } from '../../system'
import {
Avatar,
Dialog,
// EmojiHash,
EthAddress,
Flex,
Heading,
Text,
} from '../../system'
import type { Account } from '../../protocol'
@ -18,10 +26,12 @@ export const DisconnectDialog = (props: Props) => {
<Dialog.Body gap="5">
<Text>Do you want to disconnect your profile from this browser?</Text>
<Flex direction="column" align="center" gap="2">
<Avatar size={64} />
<Avatar size={64} name={account.username} />
<Heading weight="600">{account.username}</Heading>
<Text color="gray">Chatkey: {account.chatKey}</Text>
<EmojiHash />
<Text color="gray">
Chatkey: <EthAddress>{account.chatKey}</EthAddress>
</Text>
{/* <EmojiHash /> */}
</Flex>
</Dialog.Body>
<Dialog.Actions>

View File

@ -26,7 +26,7 @@ export const MemberItem = (props: Props) => {
/>
<div>
<Flex align="center" gap={1}>
<Text size="15" color="accent" truncate>
<Text size="15" color="accent" truncate css={{ width: 184 }}>
{username}
</Text>
{verified && (

View File

@ -14,12 +14,12 @@ export const UserItem = (props: Props) => {
const { account } = props
return (
<Flex align="center" justify="between">
<Flex align="center" justify="between" gap={1}>
<Flex gap="2" align="center" css={{ height: 56 }}>
<Avatar size={32} name={account.username} />
<div>
<Flex align="center" gap={1}>
<Text size="15" color="accent">
<Text size="15" color="accent" truncate css={{ width: 144 }}>
{account.username}
</Text>
</Flex>
@ -27,29 +27,28 @@ export const UserItem = (props: Props) => {
{account.chatKey}
</EthAddress>
</div>
<DialogTrigger>
<DisconnectButton>
<svg
width="20"
height="18"
viewBox="0 0 20 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15.985 8.36269C16.363 8.36269 16.5523 7.90572 16.285 7.63846L14.7017 6.05509C14.4531 5.80658 14.4531 5.40365 14.7017 5.15514C14.9502 4.90662 15.3531 4.90662 15.6016 5.15514L18.9956 8.54908C19.2441 8.79759 19.2441 9.20051 18.9956 9.44903L15.6016 12.843C15.3531 13.0915 14.9502 13.0915 14.7017 12.843C14.4531 12.5945 14.4531 12.1915 14.7017 11.943L16.285 10.3596C16.5523 10.0924 16.363 9.63542 15.985 9.63542L7.51527 9.63542C7.16382 9.63542 6.87891 9.35051 6.87891 8.99905C6.87891 8.6476 7.16382 8.36269 7.51527 8.36269L15.985 8.36269Z"
fill="#4360DF"
/>
<path
d="M11.1218 3.90956C11.1218 2.73805 10.1721 1.78835 9.00059 1.78835H3.90968C2.73817 1.78835 1.78847 2.73805 1.78847 3.90956V14.0914C1.78847 15.2629 2.73817 16.2126 3.90968 16.2126H9.00059C10.1721 16.2126 11.1218 15.2629 11.1218 14.0914V11.3338C11.1218 10.9824 11.4067 10.6974 11.7582 10.6974C12.1096 10.6974 12.3945 10.9824 12.3945 11.3338V14.0914C12.3945 15.9658 10.875 17.4853 9.00059 17.4853H3.90968C2.03526 17.4853 0.515744 15.9658 0.515744 14.0914V3.90956C0.515744 2.03514 2.03526 0.515625 3.90968 0.515625H9.00059C10.875 0.515625 12.3945 2.03514 12.3945 3.90956V6.66714C12.3945 7.01859 12.1096 7.3035 11.7582 7.3035C11.4067 7.3035 11.1218 7.01859 11.1218 6.66714V3.90956Z"
fill="#4360DF"
/>
</svg>
</DisconnectButton>
<DisconnectDialog account={account} />
</DialogTrigger>
</Flex>
<DialogTrigger>
<DisconnectButton>
<svg
width="20"
height="18"
viewBox="0 0 20 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15.985 8.36269C16.363 8.36269 16.5523 7.90572 16.285 7.63846L14.7017 6.05509C14.4531 5.80658 14.4531 5.40365 14.7017 5.15514C14.9502 4.90662 15.3531 4.90662 15.6016 5.15514L18.9956 8.54908C19.2441 8.79759 19.2441 9.20051 18.9956 9.44903L15.6016 12.843C15.3531 13.0915 14.9502 13.0915 14.7017 12.843C14.4531 12.5945 14.4531 12.1915 14.7017 11.943L16.285 10.3596C16.5523 10.0924 16.363 9.63542 15.985 9.63542L7.51527 9.63542C7.16382 9.63542 6.87891 9.35051 6.87891 8.99905C6.87891 8.6476 7.16382 8.36269 7.51527 8.36269L15.985 8.36269Z"
fill="#4360DF"
/>
<path
d="M11.1218 3.90956C11.1218 2.73805 10.1721 1.78835 9.00059 1.78835H3.90968C2.73817 1.78835 1.78847 2.73805 1.78847 3.90956V14.0914C1.78847 15.2629 2.73817 16.2126 3.90968 16.2126H9.00059C10.1721 16.2126 11.1218 15.2629 11.1218 14.0914V11.3338C11.1218 10.9824 11.4067 10.6974 11.7582 10.6974C12.1096 10.6974 12.3945 10.9824 12.3945 11.3338V14.0914C12.3945 15.9658 10.875 17.4853 9.00059 17.4853H3.90968C2.03526 17.4853 0.515744 15.9658 0.515744 14.0914V3.90956C0.515744 2.03514 2.03526 0.515625 3.90968 0.515625H9.00059C10.875 0.515625 12.3945 2.03514 12.3945 3.90956V6.66714C12.3945 7.01859 12.1096 7.3035 11.7582 7.3035C11.4067 7.3035 11.1218 7.01859 11.1218 6.66714V3.90956Z"
fill="#4360DF"
/>
</svg>
</DisconnectButton>
<DisconnectDialog account={account} />
</DialogTrigger>
</Flex>
)
}
@ -62,4 +61,5 @@ const DisconnectButton = styled('button', {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
})

View File

@ -1,95 +0,0 @@
import { useCallback, useEffect, useState } from 'react'
// See: https://usehooks-ts.com/react-hook/use-event-listener
// import { useEventListener } from '../useEventListener'
import type { Dispatch, SetStateAction } from 'react'
declare global {
interface WindowEventMap {
'local-storage': CustomEvent
}
}
type SetValue<T> = Dispatch<SetStateAction<T>>
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, SetValue<T>] {
// Get from local storage then
// parse stored json or return initialValue
const readValue = useCallback((): T => {
// Prevent build error "window is undefined" but keep keep working
if (typeof window === 'undefined') {
return initialValue
}
try {
const item = window.localStorage.getItem(key)
return item ? (parseJSON(item) as T) : initialValue
} catch (error) {
console.warn(`Error reading localStorage key “${key}”:`, error)
return initialValue
}
}, [initialValue, key])
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState<T>(readValue)
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue: SetValue<T> = useCallback(
value => {
// Prevent build error "window is undefined" but keeps working
if (typeof window == 'undefined') {
console.warn(
`Tried setting localStorage key “${key}” even though environment is not a client`
)
}
try {
// Allow value to be a function so we have the same API as useState
const newValue = value instanceof Function ? value(storedValue) : value
// Save to local storage
window.localStorage.setItem(key, JSON.stringify(newValue))
// Save state
setStoredValue(newValue)
// We dispatch a custom event so every useLocalStorage hook are notified
window.dispatchEvent(new Event('local-storage'))
} catch (error) {
console.warn(`Error setting localStorage key “${key}”:`, error)
}
},
[key, storedValue]
)
useEffect(() => {
setStoredValue(readValue())
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// const handleStorageChange = useCallback(() => {
// setStoredValue(readValue())
// }, [readValue])
// this only works for other documents, not the current one
// useEventListener('storage', handleStorageChange)
// this is a custom event, triggered in writeValueToLocalStorage
// See: useLocalStorage()
// useEventListener('local-storage', handleStorageChange)
return [storedValue, setValue]
}
function parseJSON<T>(value: string | null): T | undefined {
try {
return value === 'undefined' ? undefined : JSON.parse(value ?? '')
} catch {
return undefined
}
}

View File

@ -19,8 +19,7 @@ type State = {
type Action =
| { type: 'INIT'; client: Client }
| { type: 'UPDATE_COMMUNITY'; community: Community['description'] }
| { type: 'SET_ACCOUNT'; account: Account }
| { type: 'REMOVE_ACCOUNT' }
| { type: 'SET_ACCOUNT'; account: Account | undefined }
interface Props {
options: ClientOptions
@ -35,6 +34,7 @@ const reducer = (state: State, action: Action): State => {
...state,
loading: false,
client,
account: client.account,
community: client.community.description,
}
}
@ -44,9 +44,6 @@ const reducer = (state: State, action: Action): State => {
case 'SET_ACCOUNT': {
return { ...state, account: action.account }
}
case 'REMOVE_ACCOUNT': {
return { ...state, account: undefined }
}
}
}
@ -66,6 +63,7 @@ export const ProtocolProvider = (props: Props) => {
useEffect(() => {
const loadClient = async () => {
const client = await createClient(options)
dispatch({ type: 'INIT', client })
}
@ -76,9 +74,18 @@ export const ProtocolProvider = (props: Props) => {
useEffect(() => {
if (client) {
return client.community.onChange(community => {
dispatch({ type: 'UPDATE_COMMUNITY', community })
})
const unsubscribe = [
client.onAccountChange(account => {
dispatch({ type: 'SET_ACCOUNT', account })
}),
client.community.onChange(community => {
dispatch({ type: 'UPDATE_COMMUNITY', community })
}),
]
return () => {
unsubscribe.forEach(fn => fn())
}
}
}, [client])

View File

@ -3,22 +3,14 @@ import { useProtocol } from './provider'
import type { Account } from '@status-im/js'
export const useAccount = () => {
const { client, account, dispatch } = useProtocol()
const { client, account } = useProtocol()
const createAccount = async () => {
const account = await client.createAccount()
dispatch({ type: 'SET_ACCOUNT', account })
// TODO: save account
return account
}
const deleteAccount = () => {
dispatch({ type: 'REMOVE_ACCOUNT' })
// TODO: remove from storage
}
return { account, createAccount, deleteAccount } as const
return {
account,
createAccount: () => client.createAccount(),
deleteAccount: () => client.deleteAccount(),
isMember: account ? client.community.isMember(account.chatKey) : false,
} as const
}
export type { Account }

View File

@ -41,7 +41,7 @@ const Avatar = (props: Props) => {
size={size}
style={{
background: identiconRing,
padding: !identiconRing ? 0 : undefined,
padding: identiconRing ? undefined : 0,
}}
>
<Content style={{ background: color }}>