diff --git a/package.json b/package.json index edcdee499..57a2a7dfd 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "@verida/vda-did-resolver": "^2.3.5", "@verida/vda-name-client": "^2.3.4", "@verida/verifiable-credentials": "^2.3.4", + "@verida/vda-sbt-client": "^2.2.1", "@verida/wallet-utils": "^1.7.4", "@walletconnect/client": "^1.7.7", "@walletconnect/sign-client": "2.0.0-rc.3", diff --git a/src/api/AssetManager.ts b/src/api/AssetManager.ts deleted file mode 100644 index 87ef63bad..000000000 --- a/src/api/AssetManager.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { walletProviderApi } from './Wallet/WalletProvider' - -import { walletBadges } from './mocks/WalletBadges' -import { walletNFTs } from './mocks/WalletNfts' -import { Badge, ClaimBadgeResponse, WalletNFTsResponse } from './types' - -export class AssetManager { - private static instance: AssetManager - - public static getInstance(): AssetManager { - if (!AssetManager.instance) { - AssetManager.instance = new AssetManager() - } - return AssetManager.instance - } - - /** - * Get wallet NFT collections - * - * Ex: walletNFTs?wallet=0x34e77AD857217D8D93dcC0bAE752E2290A2EFb66&limit=10 - */ - public async getWalletNFTCollections(params: { - wallet: string - limit: number - cursor?: string - }): Promise { - // const wallet = 'eip155:1:0x12345678901234567890123456789012345622312' - // const limit = 10 // # items per page - - // const response = await walletProviderApi.get( - // `nfts?${`wallet=${params.wallet}limit=${params.limit}`}` - // ) - // return response.data.data - - // Mock data - return walletNFTs - } - - /** - * Get wallet badges - * - * Ex: walletBadges?wallet=0x34e77AD857217D8D93dcC0bAE752E2290A2EFb66 - */ - public async getWalletBadges(wallet: string): Promise { - // ex: walletBadges?wallet=0x34e77AD857217D8D93dcC0bAE752E2290A2EFb66 - // const response = await walletProviderApi.get( - // `walletBadges?wallet=${wallet}` - // ) - // return response.data.data - - // Mock data - return walletBadges - } - - /** - * Claim badge for a wallet - */ - public async claimBadge( - wallet: string, - assetId: string - ): Promise { - // const request = await walletProviderApi.post(`claimBadge`, { - // wallet, - // assetId, - // }) - - return { badge: walletBadges[0], success: true } - } -} diff --git a/src/api/DataConnectorsManager.ts b/src/api/DataConnectorsManager.ts index 9f7c09bcc..a16123712 100644 --- a/src/api/DataConnectorsManager.ts +++ b/src/api/DataConnectorsManager.ts @@ -6,6 +6,7 @@ import { Linking } from 'react-native' import CONFIG from '../config/environment' import AccountManager from './AccountManager' +import { SBTManager } from './SBTManager' const DATA_CONNECTION_SCHEMA = 'https://vault.schemas.verida.io/data-connections/connection/v0.1.0/schema.json' @@ -20,24 +21,9 @@ const delay = async (ms: number) => { await new Promise((resolve: any) => setTimeout(() => resolve(), ms)) } -// possible states for status: syncing, disabled, active +let CONNECTION_CACHE: any -// @todo: Pull this from the server -const FacebookIcon = require('assets/social_icons/facebook.png') -const TwitterIcon = require('assets/social_icons/twitter.png') - -const CONNECTIONS: any = { - facebook: { - name: 'facebook', - label: 'Facebook', - icon: FacebookIcon, - }, - twitter: { - name: 'twitter', - label: 'Twitter', - icon: TwitterIcon, - }, -} +// possible states for status: syncing, disabled, active class DataConnectorsEvents extends EventEmitter { private static instance: DataConnectorsEvents @@ -89,7 +75,19 @@ export default class DataConnectorsManager { return DataConnectorsManager.datastore } - static getConnectionInfo(connectorName: string) { + static async getConnections(): Promise> { + if (CONNECTION_CACHE) { + return CONNECTION_CACHE + } + + // @todo cache + const response = await axios.get(`${CONFIG.DATA_CONNECTOR_URL}/providers`) + CONNECTION_CACHE = response.data + return CONNECTION_CACHE + } + + static async getConnectionInfo(connectorName: string) { + const CONNECTIONS = await DataConnectorsManager.getConnections() return CONNECTIONS[connectorName] } @@ -98,7 +96,15 @@ export default class DataConnectorsManager { return DataConnectorsManager._connections[connectorName] } - const connector = new DataConnection(connectorName) + const connectionInfo = await DataConnectorsManager.getConnectionInfo( + connectorName + ) + + const connector = new DataConnection( + connectorName, + connectionInfo.icon, + connectionInfo.label + ) await connector.init() DataConnectorsManager._connections[connectorName] = connector @@ -106,7 +112,9 @@ export default class DataConnectorsManager { } static async getConnectors(): Promise { - const connections: any = Object.values(CONNECTIONS) + const connections: any = Object.values( + await DataConnectorsManager.getConnections() + ) const connectors: any = {} for (let i = 0; i < connections.length; i++) { const connection = await DataConnectorsManager.getConnection( @@ -119,7 +127,9 @@ export default class DataConnectorsManager { } static async resetConnector() { - const connections: any = Object.values(CONNECTIONS) + const connections: any = Object.values( + await DataConnectorsManager.getConnections() + ) for (let i = 0; i < connections.length; i++) { if ( DataConnectorsManager._connections[connections[i].name].syncStatus !== @@ -178,12 +188,12 @@ class DataConnection extends EventEmitter { public metadata?: any public icon?: any - constructor(name: string) { + constructor(name: string, icon: string, label: string) { super() this.name = this.source = name this.syncStatus = 'disabled' - this.icon = CONNECTIONS[this.name].icon - this.label = CONNECTIONS[this.name].label + this.icon = icon + this.label = label this.syncFrequency = 'hour' } @@ -376,9 +386,31 @@ class DataConnection extends EventEmitter { const syncRequest = await externalDatastore.get(syncRequestId) if (syncRequest.status === 'complete') { - // Sync has completed on the server, so complete the sync - // by replicating data from the server - this.syncReplication(serverDid, contextName, syncRequest) + // Sync has completed on the server + + // Save a SBT credential if it was provided + if (syncRequest.syncInfo.profile.credential) { + const sbtManager = new SBTManager() + const profileCredentialId = `${syncRequest.source}-${syncRequest.syncInfo.profile.id}-profile` + console.log( + '===> SBT credential', + JSON.stringify( + { + profileCredentialId, + cred: syncRequest.syncInfo.profile.credential, + }, + null, + 2 + ) + ) + await sbtManager.saveCredential( + profileCredentialId, + syncRequest.syncInfo.profile.credential + ) + } + + // Complete the sync by replicating data from the server + await this.syncReplication(serverDid, contextName, syncRequest) } else { if (retryCount === 0) { // Retry count limit hit diff --git a/src/api/SBTManager.ts b/src/api/SBTManager.ts new file mode 100644 index 000000000..c9cd88f32 --- /dev/null +++ b/src/api/SBTManager.ts @@ -0,0 +1,429 @@ +import EncryptionUtils from '@verida/encryption-utils' +import { + buildVeridaUri, + explodeDID, + explodeVeridaUri, + wrapUri, +} from '@verida/helpers' +import { + DatabasePermissionOptionsEnum, + EnvironmentType, + IContext, + Web3CallType, +} from '@verida/types' +import { VeridaSBTClient } from '@verida/vda-sbt-client' +import { Credentials } from '@verida/verifiable-credentials' +import { + ClaimBadgeQuery, + UserBadge, + VeridaBadge, + VeridaBadgesMetadata, + VeridaBagdes, +} from 'features/badges/@types' +import _ from 'lodash' + +import CONFIG from '../config/environment' +import AccountManager from './AccountManager' +import { Account } from './types' + +const SCHEMA_SBT = + 'https://common.schemas.verida.io/token/sbt/storage/v0.1.0/schema.json' +const SCHEMA_CREDENTIALS = + 'https://common.schemas.verida.io/credential/base/v0.2.0/schema.json' + +export class SBTManager { + private client?: VeridaSBTClient + private static instance: SBTManager + + public static getInstance(): SBTManager { + if (!SBTManager.instance) { + SBTManager.instance = new SBTManager() + } + + return SBTManager.instance + } + + /** + * Save a SBT credential + * + * @param id + * @param credential + * @param forceUpdate + */ + public async saveCredential(id: string, credential: any) { + console.log('saveCredential()') + const context = ( + await AccountManager.getInstance().getVeridaContext() + ) + + // Open the datastore + const datastore = await context.openDatastore(SCHEMA_CREDENTIALS, {}) + const credentialRecord = { + _id: id, + ...credential, + } + + // Try to fetch an existing record + let existingRecord + try { + existingRecord = await datastore.get(id, {}) + + // Record exists, + const { _rev } = existingRecord + + if ( + !_.isEqual( + existingRecord.credentialData, + credentialRecord.credentialData + ) + ) { + // Data has changed, need to update + // Set the existing record revision so update process correctly + credentialRecord._rev = _rev + + // Data that changed + //const changes = _.differenceWith(_.toPairs(existingRecord.credentialData), _.toPairs(credentialRecord.credentialData), _.isEqual) + //console.log(changes) + + // Save the credential record + if (!(await datastore.save(credentialRecord, {}))) { + console.log('Invalid SBT credential', datastore.errors) + } + } + } catch (err) { + // Record doesn't exist, create it + await datastore.save(credentialRecord, { + forceInsert: true, + }) + } + } + + public async isMinted(credentialRecord: any) { + console.log('isMinted?', credentialRecord) + const client = await this.getClient() + console.log(credentialRecord.credentialData.did.toLowerCase()) + + try { + // API expects a wallet address + const claimedSbts = await client.getClaimedSBTList( + '0x326b857912CE962b9805881589287d786267844A' //credentialRecord.credentialData.did.toLowerCase() + ) + console.log(claimedSbts) + } catch (err) { + console.log(err.message) + } + } + + public async burnSbt( + credentialRecord: any, + mintAddress: string + ): Promise { + console.log('burnSbt', mintAddress) + mintAddress = mintAddress.toLowerCase() + const context = ( + await AccountManager.getInstance().getVeridaContext() + ) + + // Open the datastore + const datastore = await context.openDatastore(SCHEMA_SBT, { + permissions: { + read: DatabasePermissionOptionsEnum.PUBLIC, + write: DatabasePermissionOptionsEnum.OWNER, + }, + }) + + // Get the minted SBT + const sbtId = `${mintAddress}-${credentialRecord._id}` + let sbtRecord + try { + sbtRecord = await datastore.get(sbtId, {}) + } catch (err) { + // doesn't exist + throw new Error(`SBT hasn't been minted`) + } + + // Delete the minted SBT from the public database + //await datastore.delete(sbtId) + + // Unmint the SBT + const client = await this.getClient() + + /*try { + const claimedSbts = await client.getClaimedSBTList(mintAddress.toLowerCase()) + console.log(claimedSbts) + } catch (err) { + console.log(err.me + ssage) + }*/ + try { + const response = await client.burnSBT(61) + console.log('burnt', response) + } catch (err) { + console.log('burn error') + console.log(err.message) + return false + } + } + + /** + * Mint a SBT + * + * @param credentialRecord + * @param mintAddress + */ + public async mintSbt( + credentialRecord: any, + mintAddress: string + ): Promise { + console.log('mintSbt', mintAddress) + mintAddress = mintAddress.toLowerCase() + // @todo: check it hasn't been minted already + //console.log(credentialRecord, mintAddress) + //return + + const context = ( + await AccountManager.getInstance().getVeridaContext() + ) + + // Open the datastore + const datastore = await context.openDatastore(SCHEMA_SBT, { + permissions: { + read: DatabasePermissionOptionsEnum.PUBLIC, + write: DatabasePermissionOptionsEnum.OWNER, + }, + }) + + // Check this SBT hasn't already been minted + const sbtId = `${mintAddress}-${credentialRecord._id}` + try { + await datastore.get(sbtId, {}) + console.log('already exists', sbtId) + // exists + // FIXME: Should handle a case sbtData exists in the datastore but the SBT does not exist on the blockchain ? + return false + } catch (err) { + // doesn't exist + } + + // Save this SBT + const sbtData: any = { + _id: sbtId, + ...credentialRecord.credentialData, + didJwtVc: credentialRecord.didJwtVc, + } + + console.log('sbtData', sbtData) + + const result: any = await datastore.save(sbtData, { + forceInsert: true, + }) + console.log(result) + console.log(datastore.errors) + const db = await datastore.getDb() + const info = await db.info() + const credentialUri = buildVeridaUri( + await context.getAccount().did(), + context.getContextName(), + info.databaseName, + sbtId, + [] + ) + console.log(credentialUri) + + //const credentialUri = 'verida://did:vda:testnet:0xcD61d79C7db8fF5F80feCacEc0aE57274F5D6dF5/Verida%20Testing:%20Fake%20Vault/token_metadata_public/5d99c6e0-d38a-11ed-8135-f5f9ab7f39c3' + + // Fetch credential record from the network + /*const credentialRecord = await fetchVeridaUri( + credentialUri, + context.getClient() + )*/ + + // Generate URL to mint that generates the metadata + const sbtUri = + wrapUri(credentialUri, 'https://data.verida.network') + '.json' + console.log('sbtUri', sbtUri) + + const credentials = new Credentials() + + //const sbtClient = await SbtController.getSbtClient() + const generatedCredential = await credentials.verifyCredential( + credentialRecord.didJwtVc, + {} + ) + //const sbtData = generatedCredential.verifiableCredential.credentialSubject + const proofs = generatedCredential.payload.vc.proofs + const vcIssuerDid = generatedCredential.payload.iss + + // Get the context proof of the issuer + // (Links their DID to the signing key of the context that signed the credential proof) + // @ts-ignore + const didClient = context.getClient().didClient + const issuerDidDoc = await didClient.get(vcIssuerDid) + const issuerContextProof = issuerDidDoc.locateContextProof( + generatedCredential.payload.vc.veridaContextName + ) + + console.log('issuerDid', vcIssuerDid) + console.log('issuerContextProof', issuerContextProof) + const { did } = explodeVeridaUri(credentialUri) + console.log('subject did', did) + const { address } = explodeDID(did) + const proofString = `${sbtData.type}-${ + sbtData.uniqueAttribute + }-${address.toLowerCase()}` + console.log('proof string', proofString) + console.log(proofs['type-unique-didAddress']) + + const signingAddress = EncryptionUtils.getSigner( + proofString, + proofs['type-unique-didAddress'] + ) + console.log('address that signed SBT string (issuer)', signingAddress) + + /* + const issuerDidAddress = '0xB3d245bC0Fa8479b1B0b200c26f8c93e4737efC3' + const requestProofMsg = `${issuerDidAddress}${signingAddress}`.toLowerCase() + const privateKeyArray = new Uint8Array( + Buffer.from(serverconfig.testing.veridaPrivateKey.slice(2), 'hex') + )*/ + //const testSign = EncryptionUtils.signData(requestProofMsg, privateKeyArray) + /*const signerContextSigner = EncryptionUtils.getSigner( + requestProofMsg, + issuerContextProof + ) + console.log( + 'requestProofMsg (issuer signing context text)', + requestProofMsg + ) + console.log( + 'signerContextSigner (issuer signing context proof)', + signerContextSigner + )*/ + /*console.log('test sign', testSign) + + console.log('these two should match') + console.log(testSign, issuerContextProof) + + const keyring = await connection.account.keyring(generatedCredential.payload.vc.veridaContextName) + + // Get keyring keys so public keys and ownership proof can be saved to the DID document + const keys = await keyring.getKeys() + console.log(keys) + + // Generate a proof that the DID controls the context public signing key that can be used on chain + const proofStringReal = `${issuerDidAddress}${keys.signPublicAddress}`.toLowerCase() + console.log('real proof string', proofStringReal) + + const signer2 = EncryptionUtils.getSigner(proofStringReal, issuerContextProof) + console.log('signer2', signer2)*/ + + //return + + // Initiate a SBT claim on-chain + + try { + const client = await this.getClient() + const mintResult = await client.claimSBT( + sbtData.type, + sbtData.uniqueAttribute, + sbtUri, + mintAddress, + proofs['type-unique-didAddress'], + issuerContextProof + ) + + console.log('mint result') + console.log(mintResult) + return true + } catch (err) { + console.log('mint error!') + console.log(err.message) + console.log(err.reason) + throw err + } + } + + // TODO: should the param has other effects or we should just mint SBT directly + public async claimBadge( + // origin: string, + // type: string, + // caipAddress: string, + // ownershipProof: string + credentialRecord: any, + mintAddress: string + ): Promise { + // console.log('claiming badge', origin, type) + return await this.mintSbt(credentialRecord, mintAddress) + } + + /** + * Get all the available badges supported by Verida + * + * Some of these will not be claimable (see `.claimable` property) + */ + public async getAvailableBadges(origin?: string): Promise { + const vault = AccountManager.getInstance().vault! + const folder = await vault.data.selectFolder('credentials') // TODO: config + const items = await folder.getMany( + { + credentialSchema: + 'https://common.schemas.verida.io/token/sbt/credential/v0.1.0/schema.json', // TODO: is this the right filter? + }, + { + sort: [{ insertedAt: 'desc' }], + } + ) + + return items.map((item: any) => ({ + id: item._id, + label: + VeridaBadgesMetadata[item.credentialData.type as VeridaBagdes].label, + attributes: item.credentialData.attributes, + description: item.credentialData.description, + did: item.credentialData.did, + didAddress: item.credentialData.didAddress, + image: + VeridaBadgesMetadata[item.credentialData.type as VeridaBagdes].image, // Hardcode for now + name: item.credentialData.name, + type: item.credentialData.type, + uniqueAttribute: item.credentialData.uniqueAttribute, + credentialItem: item, + })) + } + + /** + * Get all the badges claimed by this user. + * + * Do we need to filter these by origin and / or type? + * + * @param origin + * @returns + */ + public async getClaimedBadges( + query: ClaimBadgeQuery = {} + ): Promise { + return [] + } + + private async getClient() { + if (this.client) { + return this.client + } + + const didClientConfig = CONFIG.VERIDA_DID_CLIENT_CONFIG + const account = ( + await AccountManager.getInstance().getSelectedAccount() + ) + + const sbtClient = new VeridaSBTClient({ + callType: didClientConfig.callType, + did: account.did, + signKey: account.privateKey, + network: CONFIG.ENVIRONMENT, + web3Options: didClientConfig.web3Config, + }) + + this.client = sbtClient + return this.client + } +} diff --git a/src/api/VaultCommon/managers/credentials.ts b/src/api/VaultCommon/managers/credentials.ts deleted file mode 100644 index 15cb1bfd4..000000000 --- a/src/api/VaultCommon/managers/credentials.ts +++ /dev/null @@ -1,38 +0,0 @@ -/*import { - VeridaApp, - Database -} from "../interfaces/VeridaApp"; - -const CREDENTIAL_DB = 'credential' - -export class CredentialsManager { - - _app: VeridaApp - _db?: Database - - constructor (app: VeridaApp) { - this._app = app - } - - // @todo - async get(credentialId: string, options: object) { - await this._init() - return this._db?.get(credentialId, options) - } - - // @todo - async getMany(filter: object, options: object) { - await this._init() - return this._db?.getMany(filter, options) - } - - async _init() { - if (this._db) { - return - } - - this._db = await this._app.openDatabase(CREDENTIAL_DB) - } - -} -*/ \ No newline at end of file diff --git a/src/api/VaultCommon/managers/data/folder.ts b/src/api/VaultCommon/managers/data/folder.ts index a3e3d9194..b8efaba70 100644 --- a/src/api/VaultCommon/managers/data/folder.ts +++ b/src/api/VaultCommon/managers/data/folder.ts @@ -98,7 +98,7 @@ export default class Folder { const layouts = json.layouts // If the schema is a credential schema with a 'credentialSubject' property, then use it. Otherwise use all the properties. - let properties = json.properties.credentialSubject + let properties = json.properties?.credentialSubject ? json.properties.credentialSubject.properties : json.properties diff --git a/src/api/VaultCommon/managers/login.ts b/src/api/VaultCommon/managers/login.ts deleted file mode 100644 index ea6531a6e..000000000 --- a/src/api/VaultCommon/managers/login.ts +++ /dev/null @@ -1,25 +0,0 @@ - -/** - * Manage login requests and responses - */ -export class LoginManager { - - _app: any - - constructor (app: any) { - this._app = app - } - - /** - * Get a list of all login requests - */ - async getMany(filter: object, options: object) {} - - /** - * Get a specific login request - * - * @param requestId - */ - async get(requestId: string) {} - -} \ No newline at end of file diff --git a/src/api/mocks/WalletBadges.ts b/src/api/mocks/WalletBadges.ts deleted file mode 100644 index 5f0a6f2c2..000000000 --- a/src/api/mocks/WalletBadges.ts +++ /dev/null @@ -1,187 +0,0 @@ -export const walletBadges = [ - { - token_address: '0xc874dadccbab2d63fb48488f572243cbe96e6dbd', - token_id: '9921', - amount: '1', - owner_of: '0x34e77ad857217d8d93dcc0bae752e2290a2efb66', - token_hash: '28d63c49d14663cc94b89bbccf881ef5', - block_number_minted: '15952664', - block_number: '15952664', - contract_type: 'ERC721', - name: 'Bored Ape Yacht Club', - symbol: 'Bored Ape Yacht Club', - token_uri: - 'https://ipfs.moralis.io:2053/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/9921', - metadata: - '{"image":"ipfs://QmWAGxnMEoGAdinkYYLpbgkfThWPscwNxV3SbrLgDmnrhq","attributes":[{"trait_type":"Hat","value":"Halo"},{"trait_type":"Background","value":"Yellow"},{"trait_type":"Fur","value":"Blue"},{"trait_type":"Clothes","value":"Hip Hop"},{"trait_type":"Mouth","value":"Bored"},{"trait_type":"Eyes","value":"3d"}]}', - last_token_uri_sync: '2022-11-12T08:13:28.977Z', - last_metadata_sync: '2022-11-12T08:13:37.062Z', - minter_address: '0x2cdef95a01e6ce18ab25316d18b16e0a40ab5386', - }, - { - token_address: '0xc874dadccbab2d63fb48488f572243cbe96e6dbd', - token_id: '9202', - amount: '1', - owner_of: '0x34e77ad857217d8d93dcc0bae752e2290a2efb66', - token_hash: '33d3accd5e8299a6ae1197d2f7674556', - block_number_minted: '15952664', - block_number: '15952664', - contract_type: 'ERC721', - name: 'Bored Ape Yacht Club', - symbol: 'Bored Ape Yacht Club', - token_uri: - 'https://ipfs.moralis.io:2053/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/9202', - metadata: - '{"image":"ipfs://QmWdCAx6eheAv8hVaHNvWjELA4tE9a2mJnP9aL9MNGfreg","attributes":[{"trait_type":"Background","value":"Orange"},{"trait_type":"Fur","value":"Dark Brown"},{"trait_type":"Mouth","value":"Phoneme Wah"},{"trait_type":"Eyes","value":"Sunglasses"},{"trait_type":"Clothes","value":"Vietnam Jacket"}]}', - last_token_uri_sync: '2022-11-12T08:13:39.294Z', - last_metadata_sync: '2022-11-12T08:13:50.029Z', - minter_address: '0x2cdef95a01e6ce18ab25316d18b16e0a40ab5386', - }, - { - token_address: '0xc874dadccbab2d63fb48488f572243cbe96e6dbd', - token_id: '8708', - amount: '1', - owner_of: '0x34e77ad857217d8d93dcc0bae752e2290a2efb66', - token_hash: '861b9ca7f7c15527617f90692460b923', - block_number_minted: '15952584', - block_number: '15952584', - contract_type: 'ERC721', - name: 'Bored Ape Yacht Club', - symbol: 'Bored Ape Yacht Club', - token_uri: - 'https://ipfs.moralis.io:2053/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/8708', - metadata: null, - last_token_uri_sync: '2022-11-12T07:57:43.845Z', - last_metadata_sync: null, - minter_address: null, - }, - { - token_address: '0xc874dadccbab2d63fb48488f572243cbe96e6dbd', - token_id: '8353', - amount: '1', - owner_of: '0x34e77ad857217d8d93dcc0bae752e2290a2efb66', - token_hash: '8c78fa365c76f4b70506a2f948582708', - block_number_minted: '15952584', - block_number: '15952584', - contract_type: 'ERC721', - name: 'Bored Ape Yacht Club', - symbol: 'Bored Ape Yacht Club', - token_uri: - 'https://ipfs.moralis.io:2053/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/8353', - metadata: - '{"image":"ipfs://QmWmeA7ycPdVHxyskfwfhBJ1ZAE3LABhGfcH727sCR7Pim","attributes":[{"trait_type":"Clothes","value":"Black Holes T"},{"trait_type":"Mouth","value":"Bored Unshaven Cigarette"},{"trait_type":"Eyes","value":"Robot"},{"trait_type":"Background","value":"Purple"},{"trait_type":"Hat","value":"Bayc Hat Black"},{"trait_type":"Fur","value":"Brown"}]}', - last_token_uri_sync: '2022-11-12T07:57:47.109Z', - last_metadata_sync: '2022-11-12T07:58:03.655Z', - minter_address: '0x751ee84d8161cdaf00cb46c2fd1ad0ec469ca2b3', - }, - { - token_address: '0xc874dadccbab2d63fb48488f572243cbe96e6dbd', - token_id: '7092', - amount: '1', - owner_of: '0x34e77ad857217d8d93dcc0bae752e2290a2efb66', - token_hash: '55cef437c59da1d78450810b107e8e4b', - block_number_minted: '15952584', - block_number: '15952584', - contract_type: 'ERC721', - name: 'Bored Ape Yacht Club', - symbol: 'Bored Ape Yacht Club', - token_uri: - 'https://ipfs.moralis.io:2053/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/7092', - metadata: null, - last_token_uri_sync: '2022-11-12T07:57:30.356Z', - last_metadata_sync: null, - minter_address: null, - }, - { - token_address: '0xc874dadccbab2d63fb48488f572243cbe96e6dbd', - token_id: '5866', - amount: '1', - owner_of: '0x34e77ad857217d8d93dcc0bae752e2290a2efb66', - token_hash: 'e19078e599e0ab09931eebcac87ff508', - block_number_minted: '15952567', - block_number: '15952567', - contract_type: 'ERC721', - name: 'Bored Ape Yacht Club', - symbol: 'Bored Ape Yacht Club', - token_uri: - 'https://ipfs.moralis.io:2053/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/5866', - metadata: null, - last_token_uri_sync: '2022-11-12T07:54:24.899Z', - last_metadata_sync: null, - minter_address: null, - }, - { - token_address: '0xc874dadccbab2d63fb48488f572243cbe96e6dbd', - token_id: '6696', - amount: '1', - owner_of: '0x34e77ad857217d8d93dcc0bae752e2290a2efb66', - token_hash: '7da10584a3bedbc6e0527951d5878e9d', - block_number_minted: '15952567', - block_number: '15952567', - contract_type: 'ERC721', - name: 'Bored Ape Yacht Club', - symbol: 'Bored Ape Yacht Club', - token_uri: - 'https://ipfs.moralis.io:2053/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/6696', - metadata: - '{"image":"ipfs://QmPgw9BRWoCKSZwsqi6wf66ZyAdNzJk6Fc3Hh6ub9wsshj","attributes":[{"trait_type":"Mouth","value":"Bored Unshaven Cigarette"},{"trait_type":"Fur","value":"Trippy"},{"trait_type":"Eyes","value":"Wide Eyed"},{"trait_type":"Hat","value":"Sushi Chef Headband"},{"trait_type":"Clothes","value":"Guayabera"},{"trait_type":"Background","value":"New Punk Blue"}]}', - last_token_uri_sync: '2022-11-12T07:54:24.870Z', - last_metadata_sync: '2022-11-12T07:54:30.575Z', - minter_address: '0x2cdef95a01e6ce18ab25316d18b16e0a40ab5386', - }, - - { - token_address: '0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d', - token_id: '1837', - amount: '1', - owner_of: '0x34e77ad857217d8d93dcc0bae752e2290a2efb66', - token_hash: '526c7780d1e540599d268498dd949d02', - block_number_minted: '12346676', - block_number: '15051739', - contract_type: 'ERC721', - name: 'BoredApeYachtClub', - symbol: 'BAYC', - token_uri: - 'https://ipfs.moralis.io:2053/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/1837', - metadata: - '{"image":"ipfs://QmXRwKKQALoTRnAbXrQGKNsgdG3VNfUqyh8T3dMmSr2ov9","attributes":[{"trait_type":"Clothes","value":"Wool Turtleneck"},{"trait_type":"Background","value":"Orange"},{"trait_type":"Fur","value":"Solid Gold"},{"trait_type":"Hat","value":"Short Mohawk"},{"trait_type":"Eyes","value":"Crazy"},{"trait_type":"Mouth","value":"Bored Unshaven"}]}', - last_token_uri_sync: '2022-10-04T14:49:23.722Z', - last_metadata_sync: '2022-10-04T14:49:27.188Z', - minter_address: '0xd387a6e4e84a6c86bd90c158c6028a58cc8ac459', - }, - { - token_address: '0xeb7d028030f3a530cac46c4bee9ab3ed2b478681', - token_id: '8708', - amount: '1', - owner_of: '0x34e77ad857217d8d93dcc0bae752e2290a2efb66', - token_hash: '17d97e93b11c037e8f46dee71922af48', - block_number_minted: '15328441', - block_number: '15328441', - contract_type: 'ERC1155', - name: 'Insane LockedKongs Origin', - symbol: 'Insane LockedKongs Origin', - token_uri: 'https://lockedkongs.net/json/8708', - metadata: null, - last_token_uri_sync: '2022-08-12T18:03:30.441Z', - last_metadata_sync: null, - minter_address: "ERC1155 tokens don't have a single minter", - }, - { - token_address: '0xbe5a4b99703fe2908ee22a6624a9e6e4d4bb296f', - token_id: '1', - amount: '1', - owner_of: '0x34e77ad857217d8d93dcc0bae752e2290a2efb66', - token_hash: 'b207b4d0f5228bb76863346a47b66168', - block_number_minted: '15351103', - block_number: '15351422', - contract_type: 'ERC1155', - name: 'First Mier Items', - symbol: 'First Mier Items', - token_uri: 'https://profilesetting.in/mier/ipfs/1', - metadata: - '{"description":"[Mier NFT](https://woodnft.site) are the second NFT collection, representing a rogue\'s gallery of playable characters ready to delve into the dungeons represented by the Founder Maps, vanquish the dwellers and other dangers within, and emerge victorious with the loot... or die trying! Each Item will have unique combinations of [traits & abilities](https://woodnft.site) that will impact gameplay, and which are captured in the NFT metadata.Learn more at [Here](https://woodnft.site)","tokenId":2952,"name":"Mier #1","short_name":"Chapol Encrown","first_name":"Chapol","last_name":"Encrown","image":"https://profilesetting.in/mier/logo.gif","image_transparent":"ipfs://Qmc6PbAZuvQy7rv1hfNMyF1yujakYvBqMEmtPpntyEY2Tb/tp_portrait_2952.png","image_card":"ipfs://QmWyAMcuqYT9yepfCwt77KywNmm1NHATFGxaXsywdekxyq/char_card_2952.png","attributes":[{"trait_type":"Brawn","value":"8"},{"trait_type":"Agility","value":"17"},{"trait_type":"Guile","value":"15"},{"trait_type":"Spirit","value":"13"},{"trait_type":"Trait","value":"Glitch Walker"},{"trait_type":"Trait","value":"Glitch: Rainbow"},{"trait_type":"art_background","value":"glitch_rainbow"},{"trait_type":"art_armor","value":"collared mezzmer"},{"trait_type":"art_head","value":"hair 52"},{"trait_type":"art_weapon","value":"morning glory"}],"traits":[{"trait":"Glitch Walker","description":"Bonus to health & defense in any glitch maps."},{"trait":"Glitch: Rainbow","description":"Random strong positive trait on every run."}],"external_url":"https://woodnft.site"}', - last_token_uri_sync: '2022-08-16T07:46:31.697Z', - last_metadata_sync: '2022-11-15T09:43:08.673Z', - minter_address: "ERC1155 tokens don't have a single minter", - }, -] diff --git a/src/api/mocks/WalletNfts.ts b/src/api/mocks/WalletNfts.ts deleted file mode 100644 index 679cc8a8e..000000000 --- a/src/api/mocks/WalletNfts.ts +++ /dev/null @@ -1,377 +0,0 @@ -export const walletNFTs = { - total: 3, - page: 1, - page_size: 10, - cursor: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjdXN0b21QYXJhbXMiOnsid2FsbGV0QWRkcmVzcyI6IjB4MzRlNzdhZDg1NzIxN2Q4ZDkzZGNjMGJhZTc1MmUyMjkwYTJlZmI2NiJ9LCJrZXlzIjpbIjE2NjQ3ODc5ODcuNjYyIl0sIndoZXJlIjp7Im93bmVyX29mIjoiMHgzNGU3N2FkODU3MjE3ZDhkOTNkY2MwYmFlNzUyZTIyOTBhMmVmYjY2In0sImxpbWl0IjozMCwib2Zmc2V0IjowLCJvcmRlciI6W10sInRvdGFsIjoyMzIsInBhZ2UiOjEsInRhaWxPZmZzZXQiOjEsImlhdCI6MTY2ODUyMDQxMH0.95N-ZJKYuFk3HDmt-S42t2J2koHxiodb-pRjqwYixu8', - collections: [ - { - token_address: '0xc874dadccbab2d63fb48488f572243cbe96e6dbd', - name: 'Bored Ape Yacht Club', - symbol: 'Bored Ape Yacht Club', - contract_type: 'ERC721', - nfts: { - total: 10, - page: 1, - page_size: 10, - cursor: null, - data: [ - { - token_address: '0xc874dadccbab2d63fb48488f572243cbe96e6dbd', - token_id: '9921', - amount: '1', - owner_of: '0x34e77ad857217d8d93dcc0bae752e2290a2efb66', - token_hash: '28d63c49d14663cc94b89bbccf881ef5', - block_number_minted: '15952664', - block_number: '15952664', - contract_type: 'ERC721', - name: 'Bored Ape Yacht Club', - symbol: 'Bored Ape Yacht Club', - token_uri: - 'https://ipfs.moralis.io:2053/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/9921', - metadata: - '{"image":"ipfs://QmWAGxnMEoGAdinkYYLpbgkfThWPscwNxV3SbrLgDmnrhq","attributes":[{"trait_type":"Hat","value":"Halo"},{"trait_type":"Background","value":"Yellow"},{"trait_type":"Fur","value":"Blue"},{"trait_type":"Clothes","value":"Hip Hop"},{"trait_type":"Mouth","value":"Bored"},{"trait_type":"Eyes","value":"3d"}]}', - last_token_uri_sync: '2022-11-12T08:13:28.977Z', - last_metadata_sync: '2022-11-12T08:13:37.062Z', - minter_address: '0x2cdef95a01e6ce18ab25316d18b16e0a40ab5386', - }, - { - token_address: '0xc874dadccbab2d63fb48488f572243cbe96e6dbd', - token_id: '9202', - amount: '1', - owner_of: '0x34e77ad857217d8d93dcc0bae752e2290a2efb66', - token_hash: '33d3accd5e8299a6ae1197d2f7674556', - block_number_minted: '15952664', - block_number: '15952664', - contract_type: 'ERC721', - name: 'Bored Ape Yacht Club', - symbol: 'Bored Ape Yacht Club', - token_uri: - 'https://ipfs.moralis.io:2053/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/9202', - metadata: - '{"image":"ipfs://QmWdCAx6eheAv8hVaHNvWjELA4tE9a2mJnP9aL9MNGfreg","attributes":[{"trait_type":"Background","value":"Orange"},{"trait_type":"Fur","value":"Dark Brown"},{"trait_type":"Mouth","value":"Phoneme Wah"},{"trait_type":"Eyes","value":"Sunglasses"},{"trait_type":"Clothes","value":"Vietnam Jacket"}]}', - last_token_uri_sync: '2022-11-12T08:13:39.294Z', - last_metadata_sync: '2022-11-12T08:13:50.029Z', - minter_address: '0x2cdef95a01e6ce18ab25316d18b16e0a40ab5386', - }, - { - token_address: '0xc874dadccbab2d63fb48488f572243cbe96e6dbd', - token_id: '8708', - amount: '1', - owner_of: '0x34e77ad857217d8d93dcc0bae752e2290a2efb66', - token_hash: '861b9ca7f7c15527617f90692460b923', - block_number_minted: '15952584', - block_number: '15952584', - contract_type: 'ERC721', - name: 'Bored Ape Yacht Club', - symbol: 'Bored Ape Yacht Club', - token_uri: - 'https://ipfs.moralis.io:2053/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/8708', - metadata: - '{"image":"ipfs://QmUoQKxfxtzV11SDhf4F5j1UbxghPdL1PAs6nksyKigTse","attributes":[{"trait_type":"Mouth","value":"Bored Unshaven Cigarette"},{"trait_type":"Fur","value":"Black"},{"trait_type":"Hat","value":"Party Hat 1"},{"trait_type":"Eyes","value":"Sunglasses"},{"trait_type":"Earring","value":"Silver Hoop"},{"trait_type":"Background","value":"Aquamarine"},{"trait_type":"Clothes","value":"Sleeveless T"}]}', - last_token_uri_sync: '2022-11-12T07:57:43.845Z', - last_metadata_sync: '2022-11-15T09:59:50.204Z', - minter_address: null, - }, - { - token_address: '0xc874dadccbab2d63fb48488f572243cbe96e6dbd', - token_id: '8353', - amount: '1', - owner_of: '0x34e77ad857217d8d93dcc0bae752e2290a2efb66', - token_hash: '8c78fa365c76f4b70506a2f948582708', - block_number_minted: '15952584', - block_number: '15952584', - contract_type: 'ERC721', - name: 'Bored Ape Yacht Club', - symbol: 'Bored Ape Yacht Club', - token_uri: - 'https://ipfs.moralis.io:2053/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/8353', - metadata: - '{"image":"ipfs://QmWmeA7ycPdVHxyskfwfhBJ1ZAE3LABhGfcH727sCR7Pim","attributes":[{"trait_type":"Clothes","value":"Black Holes T"},{"trait_type":"Mouth","value":"Bored Unshaven Cigarette"},{"trait_type":"Eyes","value":"Robot"},{"trait_type":"Background","value":"Purple"},{"trait_type":"Hat","value":"Bayc Hat Black"},{"trait_type":"Fur","value":"Brown"}]}', - last_token_uri_sync: '2022-11-12T07:57:47.109Z', - last_metadata_sync: '2022-11-12T07:58:03.655Z', - minter_address: '0x751ee84d8161cdaf00cb46c2fd1ad0ec469ca2b3', - }, - { - token_address: '0xc874dadccbab2d63fb48488f572243cbe96e6dbd', - token_id: '7092', - amount: '1', - owner_of: '0x34e77ad857217d8d93dcc0bae752e2290a2efb66', - token_hash: '55cef437c59da1d78450810b107e8e4b', - block_number_minted: '15952584', - block_number: '15952584', - contract_type: 'ERC721', - name: 'Bored Ape Yacht Club', - symbol: 'Bored Ape Yacht Club', - token_uri: - 'https://ipfs.moralis.io:2053/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/7092', - metadata: - '{"image":"ipfs://QmRsUowukNhNdUeJJjLSxwQU8eJLZFnvmiFW8avQpSKmCc","attributes":[{"trait_type":"Clothes","value":"Striped Tee"},{"trait_type":"Mouth","value":"Bored"},{"trait_type":"Eyes","value":"Bored"},{"trait_type":"Fur","value":"Golden Brown"},{"trait_type":"Hat","value":"Girl\'s Hair Short"},{"trait_type":"Background","value":"New Punk Blue"}]}', - last_token_uri_sync: '2022-11-12T07:57:30.356Z', - last_metadata_sync: '2022-11-15T09:59:50.204Z', - minter_address: null, - }, - { - token_address: '0xc874dadccbab2d63fb48488f572243cbe96e6dbd', - token_id: '5866', - amount: '1', - owner_of: '0x34e77ad857217d8d93dcc0bae752e2290a2efb66', - token_hash: 'e19078e599e0ab09931eebcac87ff508', - block_number_minted: '15952567', - block_number: '15952567', - contract_type: 'ERC721', - name: 'Bored Ape Yacht Club', - symbol: 'Bored Ape Yacht Club', - token_uri: - 'https://ipfs.moralis.io:2053/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/5866', - metadata: - '{"image":"ipfs://QmaugDVgNtJQprPKvrnxf7h4ScHY1cVq4pzv6D81wAGoZT","attributes":[{"trait_type":"Earring","value":"Silver Hoop"},{"trait_type":"Eyes","value":"Bored"},{"trait_type":"Mouth","value":"Bored"},{"trait_type":"Hat","value":"Seaman\'s Hat"},{"trait_type":"Background","value":"Yellow"},{"trait_type":"Clothes","value":"Prom Dress"},{"trait_type":"Fur","value":"Cream"}]}', - last_token_uri_sync: '2022-11-12T07:54:24.899Z', - last_metadata_sync: '2022-11-15T09:59:50.204Z', - minter_address: null, - }, - { - token_address: '0xc874dadccbab2d63fb48488f572243cbe96e6dbd', - token_id: '6696', - amount: '1', - owner_of: '0x34e77ad857217d8d93dcc0bae752e2290a2efb66', - token_hash: '7da10584a3bedbc6e0527951d5878e9d', - block_number_minted: '15952567', - block_number: '15952567', - contract_type: 'ERC721', - name: 'Bored Ape Yacht Club', - symbol: 'Bored Ape Yacht Club', - token_uri: - 'https://ipfs.moralis.io:2053/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/6696', - metadata: - '{"image":"ipfs://QmPgw9BRWoCKSZwsqi6wf66ZyAdNzJk6Fc3Hh6ub9wsshj","attributes":[{"trait_type":"Mouth","value":"Bored Unshaven Cigarette"},{"trait_type":"Fur","value":"Trippy"},{"trait_type":"Eyes","value":"Wide Eyed"},{"trait_type":"Hat","value":"Sushi Chef Headband"},{"trait_type":"Clothes","value":"Guayabera"},{"trait_type":"Background","value":"New Punk Blue"}]}', - last_token_uri_sync: '2022-11-12T07:54:24.870Z', - last_metadata_sync: '2022-11-12T07:54:30.575Z', - minter_address: '0x2cdef95a01e6ce18ab25316d18b16e0a40ab5386', - }, - { - token_address: '0xc874dadccbab2d63fb48488f572243cbe96e6dbd', - token_id: '4359', - amount: '1', - owner_of: '0x34e77ad857217d8d93dcc0bae752e2290a2efb66', - token_hash: 'cc84ac4e6facddf54011c24a857ddc8c', - block_number_minted: '15952538', - block_number: '15952538', - contract_type: 'ERC721', - name: 'Bored Ape Yacht Club', - symbol: 'Bored Ape Yacht Club', - token_uri: - 'https://ipfs.moralis.io:2053/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/4359', - metadata: - '{"image":"ipfs://QmcNgtnfcz3LKHTHPzYrGqdGbRieLZ4vGjV6XCPGNK8tDc","attributes":[{"trait_type":"Earring","value":"Silver Hoop"},{"trait_type":"Hat","value":"Spinner Hat"},{"trait_type":"Clothes","value":"Striped Tee"},{"trait_type":"Eyes","value":"Wide Eyed"},{"trait_type":"Mouth","value":"Bored Unshaven"},{"trait_type":"Background","value":"Purple"},{"trait_type":"Fur","value":"Black"}]}', - last_token_uri_sync: '2022-11-12T07:48:34.369Z', - last_metadata_sync: '2022-11-12T07:48:41.553Z', - minter_address: '0x07246c91dbf58dd091821070dd8d06cc4e0289bc', - }, - { - token_address: '0xc874dadccbab2d63fb48488f572243cbe96e6dbd', - token_id: '2263', - amount: '1', - owner_of: '0x34e77ad857217d8d93dcc0bae752e2290a2efb66', - token_hash: '2d808d797b9c994b536017fc73909d2d', - block_number_minted: '15952518', - block_number: '15952518', - contract_type: 'ERC721', - name: 'Bored Ape Yacht Club', - symbol: 'Bored Ape Yacht Club', - token_uri: - 'https://ipfs.moralis.io:2053/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/2263', - metadata: - '{"image":"ipfs://QmehsrnAdrPwZaEyaXg49WWecYkxCqfVSzqMRrXHa5AfWS","attributes":[{"trait_type":"Fur","value":"Dark Brown"},{"trait_type":"Background","value":"Blue"},{"trait_type":"Eyes","value":"Scumbag"},{"trait_type":"Clothes","value":"Leather Jacket"},{"trait_type":"Mouth","value":"Bored"}]}', - last_token_uri_sync: '2022-11-12T07:44:44.938Z', - last_metadata_sync: '2022-11-12T07:44:48.357Z', - minter_address: '0x2cdef95a01e6ce18ab25316d18b16e0a40ab5386', - }, - { - token_address: '0xc874dadccbab2d63fb48488f572243cbe96e6dbd', - token_id: '1700', - amount: '1', - owner_of: '0x34e77ad857217d8d93dcc0bae752e2290a2efb66', - token_hash: 'b83790666c923d92fdfec4843d253d33', - block_number_minted: '15952518', - block_number: '15952518', - contract_type: 'ERC721', - name: 'Bored Ape Yacht Club', - symbol: 'Bored Ape Yacht Club', - token_uri: - 'https://ipfs.moralis.io:2053/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/1700', - metadata: - '{"image":"ipfs://QmXeBN1QNPkHDqD4rHic2oavN2VYLSyok2gag2bxvEKeTR","attributes":[{"trait_type":"Mouth","value":"Rage"},{"trait_type":"Eyes","value":"Closed"},{"trait_type":"Clothes","value":"Work Vest"},{"trait_type":"Hat","value":"Halo"},{"trait_type":"Fur","value":"Black"},{"trait_type":"Background","value":"Blue"}]}', - last_token_uri_sync: '2022-11-12T07:44:44.935Z', - last_metadata_sync: '2022-11-12T07:44:48.405Z', - minter_address: '0x2cdef95a01e6ce18ab25316d18b16e0a40ab5386', - }, - ], - }, - }, - { - token_address: '0xf95a009fdc6f11d7b8d1431a3cffea5d28c5b66a', - contract_type: 'ERC1155', - name: 'Top Fuds Cards', - symbol: 'Top Fuds Cards', - nfts: { - total: 1, - page: 1, - page_size: 10, - cursor: null, - data: [ - { - token_address: '0xf95a009fdc6f11d7b8d1431a3cffea5d28c5b66a', - token_id: '1', - owner_of: '0x34e77ad857217d8d93dcc0bae752e2290a2efb66', - block_number: '15384246', - block_number_minted: '15383900', - token_hash: '5b6f6633a7bb7bcf01ba870425be085d', - amount: '1', - contract_type: 'ERC1155', - name: 'Top Fuds Cards', - symbol: 'Top Fuds Cards', - token_uri: 'https://fudnft.site/ipfs/1', - metadata: - '{"animation_url":"ipfs://QmPt4rxSeL28LFBd6AK8Vs9tEzHj6RPSPALDuMNBumpGMc","name":"FUD #1","description":"[The FUD token](https://fudnft.site) serves as the introduction of transparent coordinated FUDing in the [NFT space](https://fudnft.site). These tokens can be staked to earn my attention, which can potentially lead to me [fudding projects!](https://fudnft.site)","image":"https://ipfs.io/ipfs/QmQRyjT3Ye6BGuWwgZmoWfqtVevojcC9LwxnwqgGczPtUA","attributes":[{"trait_type":"Fud Level","value":"100"}],"external_url":"https://fudnft.site"}', - last_token_uri_sync: '2022-08-21T12:06:31.099Z', - last_metadata_sync: '2022-11-23T07:50:53.622Z', - minter_address: "ERC1155 tokens don't have a single minter", - }, - ], - status: 'SYNCED', - }, - }, - { - token_address: '0xa2103293ac8026d6efb50c99ee6ba0a2242bf1ba', - contract_type: 'ERC1155', - name: 'Wonderful Benzi Bananas VIP Pass', - symbol: 'Wonderful Benzi Bananas VIP Pass', - nfts: { - total: 1, - page: 1, - page_size: 100, - cursor: null, - data: [ - { - token_address: '0xa2103293ac8026d6efb50c99ee6ba0a2242bf1ba', - token_id: '1', - owner_of: '0x34e77ad857217d8d93dcc0bae752e2290a2efb66', - block_number: '15357456', - block_number_minted: '15357325', - token_hash: '6951a0d001034dac320e1f851a155d69', - amount: '1', - contract_type: 'ERC1155', - name: 'Wonderful Benzi Bananas VIP Pass', - symbol: 'Wonderful Benzi Bananas VIP Pass', - token_uri: 'https://benzibananas.com/ipfs/1', - metadata: - '{"name":"Benzi Bananas : Membership Pass","symbol":"BB","description":"[Benzi Bananas Membership Pass](https://www.benzibananas.com) is your key to accessing the upcoming play-to-earn model within [Benzi Bananas](https://www.benzibananas.com) . Ownership of a Benzi Bananas Membership Pass NFT will enable its owners to earn special tokens and that will also allow those tokens to be swapped for ApeCoin.","animation_url":"https://ipfs.io/ipfs/QmT9kprLTqXrL9Lrw3YHpX4iFP1xWihjmkEJHxv6d4xXeE/animation.mp4","image":"https://ipfs.io/ipfs/QmT9kprLTqXrL9Lrw3YHpX4iFP1xWihjmkEJHxv6d4xXeE/pass.gif","external_url":"https://www.benzibananas.com","attributes":[]}', - last_token_uri_sync: '2022-08-17T07:17:30.469Z', - last_metadata_sync: '2022-12-08T13:51:00.683Z', - minter_address: "ERC1155 tokens don't have a single minter", - }, - ], - status: 'SYNCED', - }, - }, - { - token_address: '0xfb24a6e7f3bc310a726882964ce6c5a12a3f0111', - contract_type: 'ERC1155', - name: 'Real RTFKT LOOT Pod', - symbol: 'Real RTFKT LOOT Pod', - nfts: { - total: 1, - page: 1, - page_size: 10, - cursor: null, - data: [ - { - token_address: '0xfb24a6e7f3bc310a726882964ce6c5a12a3f0111', - token_id: '55', - owner_of: '0x34e77ad857217d8d93dcc0bae752e2290a2efb66', - block_number: '15450354', - block_number_minted: '15450354', - token_hash: '0c2e65c23c38140ff9fc503b461c3625', - amount: '1', - contract_type: 'ERC1155', - name: 'Real RTFKT LOOT Pod', - symbol: 'Real RTFKT LOOT Pod', - token_uri: 'https://www.oncybers.app/ipfs/55', - metadata: - '{"name":"RTFKT LOOT Pod","symbol":"LOOTPOD","description":"PodX is the core of RTFKT Pod ecosystem. This collection is dedicated to RTFKT rooms, experiences, furniture design, made by RTFKT and its collaborators. The Metaverse built, step by step, together. Powered with Cyber. Visit (https://onpodx.net) to get more.","image":"https://onpodx.net/1.png","destination_url":"ipfs://QmRQuBK9ExQa3jKRr4EVrZ4wVHFd6dECJWkAvXFun58Je4","external_url":"https://onpodx.net","attributes":[]}', - last_token_uri_sync: '2022-09-01T02:51:49.125Z', - last_metadata_sync: '2022-11-16T09:06:52.825Z', - minter_address: "ERC1155 tokens don't have a single minter", - }, - ], - status: 'SYNCED', - }, - }, - { - token_address: '0xab067a74f98b0dc6ac723927df9cf612d693d205', - contract_type: 'ERC1155', - name: 'Story Of APE Key Cards', - symbol: 'Story Of APE Key Cards', - nfts: { - total: 1, - page: 1, - page_size: 100, - cursor: null, - data: [ - { - token_address: '0xab067a74f98b0dc6ac723927df9cf612d693d205', - token_id: '1', - owner_of: '0x34e77ad857217d8d93dcc0bae752e2290a2efb66', - block_number: '15374752', - block_number_minted: '15368887', - token_hash: '2cad87d5e379f4bf4af5dfb2d48d730b', - amount: '1', - contract_type: 'ERC1155', - name: 'Story Of APE Key Cards', - symbol: 'Story Of APE Key Cards', - token_uri: 'https://apelist.tech/ipfs/1', - metadata: - '{"description":"[Your key](https://apelist.tech) into the Temple of Opportunity - which gives you access to exclusive whitelists, tools, and web3 opportunities. The Ape List is a [private alpha community](https://apelist.tech) exclusive to holders of CloneX, AZUKI, Meebits and Damien Hirst. PLEASE NOTE: the utility of this NFT is exclusive to the before mentioned collections.","external_url":"https://apelist.tech","animation_url":"ipfs://QmewN5YoHbtxdWCyH26eVvhtDp5YhNZsSDga3D6A97A6fP/","image":"https://ipfs.io/ipfs/QmYWpngU7UNobDmAVQYUYjoZP6xLoc8hqRvzBKjYEwQUrx","name":"APE List #1","attributes":{}}', - last_token_uri_sync: '2022-08-19T03:07:35.677Z', - last_metadata_sync: '2022-12-08T23:23:32.310Z', - minter_address: "ERC1155 tokens don't have a single minter", - }, - ], - status: 'SYNCED', - }, - }, - { - token_address: '0xa962ff332d021ea23b95207db8726bbae77c4a7f', - contract_type: 'ERC1155', - name: 'The DragonBall Legend Limited', - symbol: 'The DragonBall Legend Limited', - nfts: { - total: 1, - page: 1, - page_size: 100, - cursor: null, - data: [ - { - token_address: '0xa962ff332d021ea23b95207db8726bbae77c4a7f', - token_id: '70', - owner_of: '0x34e77ad857217d8d93dcc0bae752e2290a2efb66', - block_number: '15600440', - block_number_minted: '15600440', - token_hash: '655de7b05b34eb068d95c1c314b30c33', - amount: '1', - contract_type: 'ERC1155', - name: 'The DragonBall Legend Limited', - symbol: 'The DragonBall Legend Limited', - token_uri: 'https://dragonballnft.shop/json/70', - metadata: null, - last_token_uri_sync: '2022-09-24T03:10:54.522Z', - last_metadata_sync: null, - minter_address: "ERC1155 tokens don't have a single minter", - }, - ], - status: 'SYNCED', - }, - }, - ], -} diff --git a/src/api/types.ts b/src/api/types.ts index a1c17a379..b98586c1c 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -10,8 +10,6 @@ import { AssetId } from 'caip' import { ImageSourcePropType } from 'react-native' -import { PLATFORM_LINKS, Platforms } from 'constants/profile' - /** * Verida Account */ @@ -174,30 +172,6 @@ export interface WalletNFTsResponse extends PagingInfo { status?: string } -// FIXME: Need to update, not the real type of badge -export interface Badge { - token_address: string - token_id: string - amount: string - owner_of: string - token_hash: string - block_number_minted: string - block_number: string - contract_type: string - name: string - symbol: string - token_uri: string | null - metadata: string | null - last_token_uri_sync: string | null - last_metadata_sync: string | null - minter_address: string | null -} - -export interface ClaimBadgeResponse { - badge?: Badge | null - success?: boolean -} - export type AddIdentityStepType = | 'CreateIdentifier' | 'ClaimUsername' @@ -220,7 +194,7 @@ export enum VeridaOnePlatformLinkCategory { } export enum VeridaOnePlatforms { - // DISCORD = 'discord', + DISCORD = 'discord', FACEBOOK = 'facebook', GITHUB = 'github', LINKEDIN = 'linkedin', diff --git a/src/assets/alert_error_icon.svg b/src/assets/alert_error_icon.svg new file mode 100644 index 000000000..ff36f3b08 --- /dev/null +++ b/src/assets/alert_error_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/alert_info_icon.svg b/src/assets/alert_info_icon.svg new file mode 100644 index 000000000..fcd367214 --- /dev/null +++ b/src/assets/alert_info_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/alert_warning_icon.svg b/src/assets/alert_warning_icon.svg new file mode 100644 index 000000000..1637a4b3f --- /dev/null +++ b/src/assets/alert_warning_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/badge_gradient_bg.png b/src/assets/badge_gradient_bg.png new file mode 100644 index 000000000..183803bd4 Binary files /dev/null and b/src/assets/badge_gradient_bg.png differ diff --git a/src/assets/badge_images/discord_account_badge.png b/src/assets/badge_images/discord_account_badge.png new file mode 100644 index 000000000..b57e5c84a Binary files /dev/null and b/src/assets/badge_images/discord_account_badge.png differ diff --git a/src/assets/badge_images/facebook_account_badge.png b/src/assets/badge_images/facebook_account_badge.png new file mode 100644 index 000000000..eb35294f7 Binary files /dev/null and b/src/assets/badge_images/facebook_account_badge.png differ diff --git a/src/assets/badge_images/twitter_account_badge.png b/src/assets/badge_images/twitter_account_badge.png new file mode 100644 index 000000000..d02d31dfd Binary files /dev/null and b/src/assets/badge_images/twitter_account_badge.png differ diff --git a/src/assets/badge_images/verida_identity_badge.png b/src/assets/badge_images/verida_identity_badge.png new file mode 100644 index 000000000..d45699d48 Binary files /dev/null and b/src/assets/badge_images/verida_identity_badge.png differ diff --git a/src/assets/icons/chevron_right_x24.svg b/src/assets/icons/chevron_right_x24.svg new file mode 100644 index 000000000..c4ac81700 --- /dev/null +++ b/src/assets/icons/chevron_right_x24.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/error_status_icon.svg b/src/assets/icons/error_status_icon.svg new file mode 100644 index 000000000..9885dfd84 --- /dev/null +++ b/src/assets/icons/error_status_icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/left_arrow_icon.svg b/src/assets/icons/left_arrow_icon.svg new file mode 100644 index 000000000..5cb791456 --- /dev/null +++ b/src/assets/icons/left_arrow_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/AddressesList/AddressesListItem.js b/src/components/AddressesList/AddressesListItem.js index 23ef69f82..7c88adc2d 100644 --- a/src/components/AddressesList/AddressesListItem.js +++ b/src/components/AddressesList/AddressesListItem.js @@ -1,34 +1,81 @@ -import { Body, Left, ListItem, Right, Text } from 'native-base' import React from 'react' -import { StyleSheet } from 'react-native' +import { Image, Pressable, StyleSheet, Text, View } from 'react-native' +import { getTruncatedWalletAddress } from 'wallet/helpers/tokens' + +import { TEXT_COLOR, WHITE_COLOR } from 'constants/color' +import { NUNITO_SANS, NUNITO_SANS_SEMIBOLD } from 'constants/text' import AddressSvg from '../../assets/icons/address.svg' import RightArrowSvg from '../../assets/icons/data/right-arrow.svg' -export default ({ item }) => { +export default ({ item, customStyles, onPress }) => { + if (!item.address) return null return ( - - - - - {item.name} - {`${item.address}`} - - - + + + + {item.icon ? ( + + ) : ( + + )} + + + {item.name || item.label} + {`${getTruncatedWalletAddress( + item.address + )}`} + {item.amount && {`${item.amount}`}} + + + - - + + ) } const styles = StyleSheet.create({ item: { - backgroundColor: '#fff', - borderRadius: 0, - marginLeft: 0, - paddingLeft: 16, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + backgroundColor: WHITE_COLOR, + borderRadius: 4, + paddingVertical: 12, + paddingHorizontal: 16, + }, + textWrapper: { + marginHorizontal: 16, + }, + label: { + fontFamily: NUNITO_SANS_SEMIBOLD, + fontWeight: '700', + fontSize: 17, + color: TEXT_COLOR, + marginBottom: 3, + marginTop: 3, + }, + icon: { + height: 64, + width: 64, + }, + address: { + fontFamily: NUNITO_SANS, + fontWeight: '400', + fontSize: 14, + color: TEXT_COLOR, + }, + amount: { + fontFamily: NUNITO_SANS, + fontWeight: '600', + fontSize: 14, + color: TEXT_COLOR, + }, + itemWrapper: { + flex: 2, + flexDirection: 'row', }, - label: { marginBottom: 3, marginTop: 3 }, - itemWrapper: { flex: 2 }, }) diff --git a/src/components/AddressesList/index.js b/src/components/AddressesList/index.js index d4182705f..d41a22da2 100644 --- a/src/components/AddressesList/index.js +++ b/src/components/AddressesList/index.js @@ -5,13 +5,13 @@ import { SwipeListView } from 'react-native-swipe-list-view' import AddressesListItem from './AddressesListItem' -export default ({ list, editButtonAction }) => { +export default ({ list, editButtonAction, onPress }) => { return ( ( - + )} renderHiddenItem={() => ( diff --git a/src/components/AppAlert/AppAlert.tsx b/src/components/AppAlert/AppAlert.tsx new file mode 100644 index 000000000..84ddf0deb --- /dev/null +++ b/src/components/AppAlert/AppAlert.tsx @@ -0,0 +1,100 @@ +import React from 'react' +import { Pressable, StyleSheet, Text, View } from 'react-native' + +import ErrorAlertIcon from 'assets/alert_error_icon.svg' +import InfoAlertIcon from 'assets/alert_info_icon.svg' +import WarningAlertIcon from 'assets/alert_warning_icon.svg' +import ChevronRightIcon from 'assets/icons/chevron_right_x24.svg' +import { NUNITO_SANS } from 'constants/text' +import { useThemeAwareStyle } from 'hooks/useThemeAwareStyle' +import { Theme } from 'styles/types' + +type AlertType = 'info' | 'warning' | 'error' + +type AppAlertProps = { + type?: AlertType + onPress?: () => void + message: string +} + +const displayAlertIcon = (alertType: AlertType): React.ReactElement => { + switch (alertType) { + case 'info': + return + case 'warning': + return + case 'error': + return + default: + return + } +} + +const AppAlert: React.FC = ({ + message, + type = 'info', + onPress, +}) => { + const styles = useThemeAwareStyle(createStyles) + return ( + + + {displayAlertIcon(type)} + {message} + + {onPress && ( + + + + + + )} + + ) +} + +export default AppAlert + +const createStyles = (theme: Theme) => { + return StyleSheet.create({ + container: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + backgroundColor: theme.color.backgroundGray, + borderLeftWidth: 3, + paddingVertical: theme.spacing.s, + paddingHorizontal: theme.spacing.m, + borderRadius: theme.borderRadius.xs, + }, + alertIcon: { + marginVertical: 2, + }, + alertContent: { + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'flex-start', + flexShrink: 1, + }, + info: { + borderLeftColor: theme.color.info, + }, + warning: { + borderLeftColor: theme.color.warning, + }, + error: { + borderLeftColor: theme.color.error, + }, + alertMessage: { + fontFamily: NUNITO_SANS, + fontWeight: '600', + fontSize: theme.fontSize.m, + lineHeight: 20, + paddingLeft: 8, + flexShrink: 1, + }, + button: { + marginLeft: 8, + }, + }) +} diff --git a/src/components/Button.js b/src/components/Button.js index 456fc4328..2edd7da54 100644 --- a/src/components/Button.js +++ b/src/components/Button.js @@ -26,6 +26,8 @@ export default function Button(props) { return 'warning' case 'transparent-warning': return 'warning' + case 'light-primary': + return 'light-primary' case 'transparent': return 'primary' case 'transparent-link': diff --git a/src/components/Navigation/NavigationHeader.tsx b/src/components/Navigation/NavigationHeader.tsx index c62b61edd..06c79d011 100644 --- a/src/components/Navigation/NavigationHeader.tsx +++ b/src/components/Navigation/NavigationHeader.tsx @@ -4,11 +4,14 @@ import { Body, Button, Header, Icon, Left, Right, Title } from 'native-base' import React from 'react' import { Platform, StyleSheet, View } from 'react-native' +import LeftArrowIcon from 'assets/left_arrow_icon.svg' import Text from 'components/Text' -import { DECLINE_COLOR, SEPARATOR_EXTRA_LIGHT } from 'constants/color' - -import LeftArrowIcon from '../../assets/left_arrow_icon.svg' -import { NUNITO_SANS_BOLD } from '../../constants/text' +import { + BLACK_COLOR, + DECLINE_COLOR, + SEPARATOR_EXTRA_LIGHT, +} from 'constants/color' +import { NUNITO_SANS_BOLD } from 'constants/text' export type HeaderSideButton = { icon: string | React.ReactElement @@ -117,7 +120,7 @@ const styles = StyleSheet.create({ fontSize: 15, }, textTitle: { - color: '#000', + color: BLACK_COLOR, fontFamily: NUNITO_SANS_BOLD, fontWeight: '600', fontSize: 17, diff --git a/src/components/PublicProfile/ProfileUsernameSection.tsx b/src/components/PublicProfile/ProfileUsernameSection.tsx index adbb15488..fd36afafc 100644 --- a/src/components/PublicProfile/ProfileUsernameSection.tsx +++ b/src/components/PublicProfile/ProfileUsernameSection.tsx @@ -9,7 +9,6 @@ import { Linking, Share, StyleSheet, - Text, TouchableOpacity, View, } from 'react-native' @@ -17,7 +16,9 @@ import Snackbar from 'react-native-snackbar' import Button from 'components/Button' import { Icon } from 'components/Icon' +import { Caption } from 'components/Typography/Caption' import { SubHeadline } from 'components/Typography/SubHeadline' +import { Text } from 'components/Typography/Text' import { VERIDA_ONE_WEBSITE } from 'constants/url' import { useThemeAwareStyle } from 'hooks/useThemeAwareStyle' import { Theme } from 'styles/types' @@ -84,15 +85,15 @@ export const ProfileUsernameSection = ({ did, username }: Props) => { Linking.openURL(buildUrl()) }}> - Go to - + @@ -111,7 +112,7 @@ export const ProfileUsernameSection = ({ did, username }: Props) => { }) }}> - { marginRight: theme.spacing.s, }}> Share - + diff --git a/src/components/SegmentControl/index.tsx b/src/components/SegmentControl/index.tsx index 48527bffc..953ddbdc5 100644 --- a/src/components/SegmentControl/index.tsx +++ b/src/components/SegmentControl/index.tsx @@ -1,10 +1,8 @@ import React, { useCallback, useImperativeHandle, useState } from 'react' import { StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native' -import { TEXT_COLOR, WHITE_COLOR } from 'constants/color' - -import { BLACK_COLOR_OPACITY } from '../../constants/color' -import { NUNITO_SANS, NUNITO_SANS_BOLD } from '../../constants/text' +import { BLACK_COLOR_OPACITY, TEXT_COLOR, WHITE_COLOR } from 'constants/color' +import { NUNITO_SANS, NUNITO_SANS_BOLD } from 'constants/text' export interface SegmentData { title?: string diff --git a/src/components/WalletList/WalletListItem.tsx b/src/components/WalletList/WalletListItem.tsx index 18c8d937e..7e8b68294 100644 --- a/src/components/WalletList/WalletListItem.tsx +++ b/src/components/WalletList/WalletListItem.tsx @@ -1,6 +1,7 @@ import { useTheme } from 'contexts/ThemeContext' import React from 'react' import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native' +import { WalletItem } from 'types/wallet' import { getTruncatedWalletAddress } from 'wallet/helpers/tokens' import CheckBoxIcon from 'assets/checkbox_icon.svg' @@ -14,8 +15,6 @@ import { } from 'constants/color' import { NUNITO_SANS } from 'constants/text' -import { WalletItem } from './types' - interface WalletListItemProps { item: WalletItem selected: boolean @@ -84,6 +83,7 @@ const styles = StyleSheet.create({ backgroundColor: WHITE_COLOR, borderColor: BLACK_COLOR_OPACITY(0.1), borderWidth: 0.2, + borderBottomWidth: 0.6, }, selectedItem: { backgroundColor: PRIMARY_COLOR_200, diff --git a/src/components/WalletList/index.tsx b/src/components/WalletList/index.tsx index e41794210..8e7583ec9 100644 --- a/src/components/WalletList/index.tsx +++ b/src/components/WalletList/index.tsx @@ -1,12 +1,11 @@ import React from 'react' import { StyleSheet, View } from 'react-native' import { SwipeListView } from 'react-native-swipe-list-view' +import { WalletItem } from 'types/wallet' import WalletListItem from 'components/WalletList/WalletListItem' import { SEPARATOR_LIGHT, WHITE_COLOR } from 'constants/color' -import { WalletItem } from './types' - interface WalletListProps { list: WalletItem[] selectedWalletId: string | number @@ -67,5 +66,8 @@ const styles = StyleSheet.create({ removeButtonOther: { marginTop: 30.5, }, - removeButtonText: { color: WHITE_COLOR, textAlign: 'center' }, + removeButtonText: { + color: WHITE_COLOR, + textAlign: 'center', + }, }) diff --git a/src/pages/Wallets/AddWalletModal.tsx b/src/components/WalletModal/AddWalletModal.tsx similarity index 90% rename from src/pages/Wallets/AddWalletModal.tsx rename to src/components/WalletModal/AddWalletModal.tsx index 1c1d54d7a..9d6dbe288 100644 --- a/src/pages/Wallets/AddWalletModal.tsx +++ b/src/components/WalletModal/AddWalletModal.tsx @@ -1,7 +1,7 @@ -import { Icon } from 'native-base' import React, { useState } from 'react' import { Modal, StyleSheet, TextInput, View } from 'react-native' +import LeftArrowIcon from 'assets/icons/left_arrow_icon.svg' import Button from 'components/Button' import Label from 'components/Label' import Layout from 'components/Layouts/Layout' @@ -14,7 +14,7 @@ type Props = { hideModal: () => void } -export default ({ visible, hideModal, onCreateNewWallet }: Props) => { +const AddWalletModal = ({ visible, hideModal, onCreateNewWallet }: Props) => { const [name, setName] = useState('') const onPressSend = () => { @@ -29,7 +29,7 @@ export default ({ visible, hideModal, onCreateNewWallet }: Props) => { visible={visible}> , + icon: , action: () => hideModal(), }} title='Create wallet' @@ -63,6 +63,8 @@ export default ({ visible, hideModal, onCreateNewWallet }: Props) => { ) } +export default AddWalletModal + const styles = StyleSheet.create({ container: { flex: 1, diff --git a/src/pages/Wallets/ImportWalletModal.tsx b/src/components/WalletModal/ImportWalletModal.tsx similarity index 98% rename from src/pages/Wallets/ImportWalletModal.tsx rename to src/components/WalletModal/ImportWalletModal.tsx index 2de6c9efa..02af2636a 100644 --- a/src/pages/Wallets/ImportWalletModal.tsx +++ b/src/components/WalletModal/ImportWalletModal.tsx @@ -13,6 +13,7 @@ import { connect } from 'react-redux' import { isValidSeedPhrase } from 'wallet/helpers/validation' import { BlockchainNetwork } from 'api/types' +import LeftArrowIcon from 'assets/icons/left_arrow_icon.svg' import Button from 'components/Button' import Label from 'components/Label' import Layout from 'components/Layouts/Layout' @@ -118,7 +119,7 @@ const ImportModal = ({ visible={visible}> , + icon: , action: () => hideModal(), }} title='Import wallet' diff --git a/src/components/WalletSelectorNavigation/WalletNavigationHeader.tsx b/src/components/WalletSelectorNavigation/WalletNavigationHeader.tsx index 6ac6e7f12..8a9a8454f 100644 --- a/src/components/WalletSelectorNavigation/WalletNavigationHeader.tsx +++ b/src/components/WalletSelectorNavigation/WalletNavigationHeader.tsx @@ -1,15 +1,15 @@ import React from 'react' import { Image, Pressable, StyleSheet, Text, View } from 'react-native' +import { WalletItem } from 'types/wallet' import { getTruncatedWalletAddress } from 'wallet/helpers/tokens' import ChevronDownIcon from 'assets/chevron_down_icon.svg' import MultichainWalletIcon from 'assets/wallet_icon_32.svg' -import { WalletItem } from 'components/WalletList/types' import { BLACK_COLOR } from 'constants/color' import { NUNITO_SANS, NUNITO_SANS_BOLD } from 'constants/text' interface WalletNavigationHeaderProps { - selectedWallet: WalletItem + selectedWallet: WalletItem | undefined openWalletModal: () => void } diff --git a/src/components/WalletSelectorNavigation/WalletSelectorModal.tsx b/src/components/WalletSelectorNavigation/WalletSelectorModal.tsx index 369e9e8a0..e87e107fe 100644 --- a/src/components/WalletSelectorNavigation/WalletSelectorModal.tsx +++ b/src/components/WalletSelectorNavigation/WalletSelectorModal.tsx @@ -5,15 +5,13 @@ import React, { useEffect, useState } from 'react' import { StyleSheet, View } from 'react-native' import { connect } from 'react-redux' import { Dispatch } from 'redux' +import { WalletItem } from 'types/wallet' import SettingsIcon from 'assets/settings_icon.svg' import Button from 'components/Button' import AppModal from 'components/modal/AppModal' import WalletList from 'components/WalletList' -import { WalletItem } from 'components/WalletList/types' import CONFIG from 'config/environment' -import { PRIMARY_COLOR, WHITE_COLOR } from 'constants/color' -import { NUNITO_SANS } from 'constants/text' import { MainStackParams } from 'navigation/types' import { selectChains } from 'reduxStore/tokens/selectors' import { setSelectedWallet } from 'reduxStore/wallet/actions' @@ -61,7 +59,6 @@ const WalletSelectorModal = ({ const ModalFooter = ( + + ) +} + +// TODO: Rework the sizing of the image. Maybe create a dedicated component +const createStyles = (theme: Theme) => { + return StyleSheet.create({ + container: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + badgeImageBackgroundContainer: { + height: 48, + width: 48, + alignItems: 'center', + justifyContent: 'center', + }, + badgeImageBackground: { + borderRadius: theme.borderRadius.xs, + }, + badgeImage: { + height: 43, + width: 37, + }, + content: { + flex: 1, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 15, + }, + textWrapper: { + flex: 1, + marginHorizontal: theme.spacing.m, + }, + title: { + fontFamily: NUNITO_SANS_SEMIBOLD, + fontWeight: '600', + fontSize: theme.fontSize.l, + color: theme.color.onBackground, + }, + subText: { + fontFamily: NUNITO_SANS, + fontWeight: '600', + fontSize: theme.fontSize.s, + color: Color(theme.color.onBackground).alpha(0.7).toString(), + }, + actionButton: { + borderRadius: 70, + paddingHorizontal: theme.spacing.sm, + paddingVertical: theme.spacing.xs, + height: 'auto', // Have to override the default style of the Button component! + marginBottom: 0, // Have to override the default style of the Button component! + borderWidth: 0, // Have to override the default style of the Button component! + }, + buttonLabel: { + // TODO: refactor + // Have to enclose the button label in its own Text to override the default style of the Button that cannot be changed. Consider a different component or improving the Button component + fontFamily: NUNITO_SANS, + fontWeight: '500', + fontSize: 12, + lineHeight: 24, + color: '#FFFFFF', + }, + }) +} diff --git a/src/features/badges/components/BadgeList.tsx b/src/features/badges/components/BadgeList.tsx new file mode 100644 index 000000000..5fb881c30 --- /dev/null +++ b/src/features/badges/components/BadgeList.tsx @@ -0,0 +1,31 @@ +import { useNavigation } from '@react-navigation/native' +import { NativeStackNavigationProp } from '@react-navigation/native-stack' +import React from 'react' +import { FlatList } from 'react-native' + +import { MainStackParams } from 'navigation/types' + +import { VeridaBadge } from '../@types' +import { BadgeItem } from './BadgeItem' + +type BadgeListProps = { + badges: VeridaBadge[] +} + +export const BadgeList: React.FC = ({ badges }) => { + const navigation = useNavigation>() + const handleClaimPress = (badge: VeridaBadge) => + navigation.navigate('ClaimBadge', { + badge, + }) + + return ( + { + return + }} + keyExtractor={(item) => item.id} + /> + ) +} diff --git a/src/features/badges/components/index.ts b/src/features/badges/components/index.ts new file mode 100644 index 000000000..815c348ad --- /dev/null +++ b/src/features/badges/components/index.ts @@ -0,0 +1,2 @@ +export * from './BadgeItem' +export * from './BadgeList' diff --git a/src/features/badges/index.ts b/src/features/badges/index.ts new file mode 100644 index 000000000..0750effe8 --- /dev/null +++ b/src/features/badges/index.ts @@ -0,0 +1,2 @@ +export * from './components' +export * from './@types' diff --git a/src/features/connections/index.ts b/src/features/connections/index.ts new file mode 100644 index 000000000..9a878f5a1 --- /dev/null +++ b/src/features/connections/index.ts @@ -0,0 +1,64 @@ +import { + Connection, + ConnectionType, + SupportedConnection, +} from 'types/connections' + +// FIXME: This is some mock data for the Badge feature. +// This information is not easily available while implementing the Badges. +// Must check DataConnectorManager.ts on how to retrieve it from the actual connections + +const FacebookIcon = require('assets/social_icons/facebook.png') +const TwitterIcon = require('assets/social_icons/twitter.png') + +/** Definitions of the supported connections. */ +export const connectionTypes: ConnectionType[] = [ + { + name: 'twitter', + label: 'Twitter', + icon: TwitterIcon, + }, + // { + // name: 'discord', + // label: 'Discord', + // icon: // Missing icon for Discord + // }, + { + name: 'facebook', + label: 'Facebook', + icon: FacebookIcon, + }, +] + +/** Data of the currently enabled connections. ie: The user is connected with an account. */ +export const connections: Connection[] = [ + { + type: 'twitter', + account: '@tahpot', + proof: 'did:vda:0x5467...78-tahpot', // Supposed to be extracted from the network + }, +] + +/** + * This method returns mock data!! + * + * Get the details of a connection. + */ +export const getConnectionType = ( + connectionName: SupportedConnection +): ConnectionType | undefined => { + return connectionTypes.find( + (connection) => connection.name === connectionName + ) +} + +/** + * This method returns mock data!! + * + * Get the data of a connection. + */ +export const getConnectionData = ( + connectionName: SupportedConnection +): Connection | undefined => { + return connections.find((connection) => connection.type === connectionName) +} diff --git a/src/hooks/useDeeplink.ts b/src/hooks/useDeeplink.ts index 204468f70..16a57e861 100644 --- a/src/hooks/useDeeplink.ts +++ b/src/hooks/useDeeplink.ts @@ -5,6 +5,7 @@ import * as Sentry from '@sentry/react-native' import parse from 'url-parse' import { DashboardTabParams, MainStackParams } from 'navigation/types' +import DataConnectorsManager from 'api/DataConnectorsManager' type NavProp = CompositeNavigationProp< BottomTabNavigationProp, @@ -12,7 +13,7 @@ type NavProp = CompositeNavigationProp< > export function useDeeplink(navigation: NavProp) { - return function (url: string) { + return async function (url: string) { try { const parsedUrl = parse(url, true) const { pathname, query } = parsedUrl @@ -29,6 +30,8 @@ export function useDeeplink(navigation: NavProp) { if (screenName === 'SingleConnection') { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore need to better typing here + query.provider = await DataConnectorsManager.getConnectionInfo(query.provider) + navigation.jumpTo('Connections') } navigation.navigate(screenName, query as never) diff --git a/src/navigation/MainNavigator.tsx b/src/navigation/MainNavigator.tsx index 83617ce30..a56320f20 100644 --- a/src/navigation/MainNavigator.tsx +++ b/src/navigation/MainNavigator.tsx @@ -12,6 +12,8 @@ import NFTCollectionDetail from 'pages/Assets/NFTCollectionDetail' import NFTDetail from 'pages/Assets/NFTDetail' import SelectAsset from 'pages/Assets/SelectAsset' import ChangePin from 'pages/Authentication/ChangePin' +import ClaimableBadges from 'pages/ClaimBadges/ClaimableBadges' +import ClaimBadge from 'pages/ClaimBadges/ClaimBadge' import SingleConnection from 'pages/Connections/SingleConnection' import Folder from 'pages/Data/Folder' import Item from 'pages/Data/Item' @@ -160,6 +162,9 @@ export const MainNavigator: React.FunctionComponent = () => { + + + = export type MainStackParams = { Inbox: undefined + ClaimableBadges: undefined + ClaimBadge: { + badgeType: Badge + } Dashboard: undefined InboxItem: { inboxItemId: string } LoginHistory: undefined diff --git a/src/pages/Assets/Badges.tsx b/src/pages/Assets/Badges.tsx index e69de29bb..f38ef0caa 100644 --- a/src/pages/Assets/Badges.tsx +++ b/src/pages/Assets/Badges.tsx @@ -0,0 +1,103 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { useNavigation } from '@react-navigation/native' +import * as sentry from '@sentry/react-native' +import { useTheme } from 'contexts/ThemeContext' +import { getNFTImageUri } from 'helpers/nft' +import React, { useCallback, useEffect } from 'react' +import { + ListRenderItem, + RefreshControl, + StyleSheet, + TouchableOpacity, + View, +} from 'react-native' +import FastImage from 'react-native-fast-image' +import { useDispatch, useSelector } from 'react-redux' +import { VeridaWallet } from 'types/wallet' + +import { NFT, NFTCollection, NFTMetadata } from 'api/types' +import NFTPlaceholder from 'assets/stubs/nft_placeholder.svg' +import { NftItem } from 'components/Assets/NftItem' +import Button from 'components/Button' +import GridView from 'components/Grids/GridView' +import { Line } from 'components/Line' +import LoadingIndicator from 'components/LoadingIndicator' +import { Tag } from 'components/Tag' +import { Title } from 'components/Typography/Title' +import { useReduxState } from 'hooks/useReduxState' +import { useThemeAwareStyle } from 'hooks/useThemeAwareStyle' +import { useGetWalletNFTCollectionsQuery } from 'reduxStore/assets/api' +import { + allWalletsSelector, + getUniqueWalletAddresses, + selectedWalletSelector, +} from 'reduxStore/wallet/selectors' +import { Theme } from 'styles/types' + +import { IMAGE_WIDTH, NUMBER_OF_COLUMNS } from './constants' + +const Badges = () => { + const dispatch = useDispatch() + const navigation = useNavigation() + const styles = useThemeAwareStyle(createStyles) + const { theme } = useTheme() + + return ( + + + + + ) +} + +const createStyles = (theme: Theme) => + StyleSheet.create({ + container: { + flex: 1, + backgroundColor: theme.color.background, + }, + grid: { + flex: 1, + paddingHorizontal: theme.spacing.m, + }, + listEmptyContainer: { height: '100%' }, + column: { + flex: 0.48, + }, + image: { + width: IMAGE_WIDTH, + minHeight: IMAGE_WIDTH, + borderRadius: theme.roundness.xs, + }, + itemTag: { + position: 'absolute', + left: theme.spacing.s, + bottom: theme.spacing.s, + }, + tagLabel: { + maxWidth: 0.68 * IMAGE_WIDTH, + color: theme.color.onPrimary, + }, + tagLabelNumber: { + marginLeft: theme.spacing.s, + color: theme.color.onPrimary, + }, + emptyListContainer: { + ...StyleSheet.absoluteFillObject, + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + marginHorizontal: theme.spacing.xxxxl, + }, + emptyListTitle: { + fontSize: theme.fontSize.xxl, + marginTop: theme.spacing.m, + textAlign: 'center', + }, + }) + +export default Badges diff --git a/src/pages/AssetsCollections.tsx b/src/pages/AssetsCollections.tsx index 5d22f9a16..33534b3f7 100644 --- a/src/pages/AssetsCollections.tsx +++ b/src/pages/AssetsCollections.tsx @@ -12,6 +12,7 @@ import WalletSelectorModal from 'components/WalletSelectorNavigation/WalletSelec import Tokens from 'pages/Tokens/Dashboard' import { getSelectedWalletById } from 'reduxStore/wallet/selectors' +import Badges from './Assets/Badges' import Collectibles from './Assets/Collectibles' const DefaultAvatar = require('assets/stubs/avatar.png') @@ -23,19 +24,18 @@ const segmentLists = [ { title: 'Collectibles', }, - // { - // title: 'Badges', - // }, + { + title: 'Badges', + }, ] - const TokensRoute = () => const CollectiblesRoute = () => -// const BadgesRoute = () => Badges +const BadgesRoute = () => const renderScene = SceneMap({ tokens: TokensRoute, nfts: CollectiblesRoute, - // badges: BadgesRoute, + badges: BadgesRoute, }) enum Assets { @@ -56,7 +56,7 @@ const AssetsCollections = (props: any) => { const [routes] = React.useState([ { key: 'tokens' }, { key: 'nfts' }, - // { key: 'badges' }, + { key: 'badges' }, ]) const onChangedSegmentIndex = (index: number) => { diff --git a/src/pages/ClaimBadges/ClaimBadge.tsx b/src/pages/ClaimBadges/ClaimBadge.tsx new file mode 100644 index 000000000..21b096988 --- /dev/null +++ b/src/pages/ClaimBadges/ClaimBadge.tsx @@ -0,0 +1,321 @@ +import { useNavigation } from '@react-navigation/native' +import { NativeStackNavigationProp } from '@react-navigation/native-stack' +import { VeridaBadge } from 'features/badges/@types' +import React, { useState } from 'react' +import { + ImageBackground, + SafeAreaView, + ScrollView, + StyleSheet, + Text, + View, +} from 'react-native' +import FastImage from 'react-native-fast-image' +import { connect } from 'react-redux' +import { WalletItem } from 'types/wallet' + +import { SBTManager } from 'api/SBTManager' +import SettingsIcon from 'assets/settings_icon.svg' +import AddressesListItem from 'components/AddressesList/AddressesListItem' +import AppAlert from 'components/AppAlert/AppAlert' +import Button from 'components/Button' +import AppModal from 'components/modal/AppModal' +import NavigationHeader from 'components/Navigation/NavigationHeader' +import WalletList from 'components/WalletList' +import CONFIG from 'config/environment' +import { NUNITO_SANS, NUNITO_SANS_SEMIBOLD } from 'constants/text' +import useParams from 'hooks/useParams' +import { useThemeAwareStyle } from 'hooks/useThemeAwareStyle' +import { MainStackParams } from 'navigation/types' +import ClaimBadgeStatus from 'pages/ClaimBadges/ClaimBadgeStatus' +import { getBlockchainNetworks } from 'reduxStore/selectors' +import { getAddressList } from 'reduxStore/wallet/selectors' +import { Theme } from 'styles/types' + +const badgeBackgroundImage = require('assets/badge_gradient_bg.png') + +const alertDesc = `Verida Badge is a public and immutable token sent to your blockchain address. It will appear on your Verida One public profile by default.` + +type Status = 'error' | 'success' | undefined + +interface ClaimBadgeProps { + addressList: WalletItem[] + defaultSelectedAddress?: WalletItem +} + +const HIT_SLOP = { top: 15, right: 15, bottom: 15, left: 15 } + +const ClaimBadge: React.FC = ({ + addressList, + defaultSelectedAddress, +}) => { + const styles = useThemeAwareStyle(createStyles) + const { badge } = useParams<{ badge: VeridaBadge }>() + const navigation = useNavigation>() + const [status, setStatus] = useState() + const [mintingBadge, setMintingBadge] = useState(false) + const [selectedAddress, setSelectedAddress] = useState< + WalletItem | undefined + >(defaultSelectedAddress) + const [modalVisible, setModalVisible] = useState(false) + // TODO: get estimated gas fee from an api for blockchain operations. + const [estimatedGasFee] = useState('0.1 MATIC (0.089 USD)') + + // TODO: Handle no data returned. ie: not connected or error + + // Have an explicit 'open' and 'close' callback to avoid unsync issue + const handleOpenModal = () => { + setModalVisible(true) + } + + const handleCloseModal = () => { + setModalVisible(false) + } + + const handleClaimAction = async () => { + try { + setMintingBadge(true) + await SBTManager.getInstance().claimBadge( + badge.credentialItem, + selectedAddress!.address! + ) + setStatus('success') + } catch (err) { + // @todo: catch error and display error message to the user + console.log(err.message) + setStatus('error') + } finally { + setMintingBadge(false) + } + } + + const handleAddressSelection = (selection: WalletItem) => { + handleCloseModal() + setSelectedAddress(selection) + } + + const handleManageWalletsPress = () => { + handleCloseModal() + navigation.navigate('ManageWallets') + } + + const ModalFooter = ( + + ) + + return ( + + + {status && ( + + + + )} + {!status && ( + + + + + + + + {badge.label} Badge + + {`${badge.description}: ${badge?.name || 'Not connected'}`} + + + + Select address + {selectedAddress && ( + + )} + + + + + + )} + {!status && ( + + {/* + Estimated gas fee + ≈ ${estimatedGasFee} + */} + + + )} + + + + + + + ) +} + +const mapStateToProps = (rootState: any) => { + const state = rootState.main + const network = CONFIG.SBT_MINT_BLOCKCHAIN + const chains = getBlockchainNetworks(rootState) + const addressList = getAddressList(state, chains, network) + + // TODO: Allow getting addresses from a list of networks, not just one + // TODO: Is network the right word? + const defaultSelectedAddress = addressList?.length > 0 ? addressList[0] : '' + console.log( + 'addressList', + JSON.stringify(addressList, null, 2), + defaultSelectedAddress + ) + // TODO: Find a better way to get the default address, maybe from the currently selected wallet. + return { + addressList, + defaultSelectedAddress, + } +} + +export default connect(mapStateToProps)(ClaimBadge) as any + +// TODO: Rework the sizing of the image. Maybe create a dedicated component +const createStyles = (theme: Theme) => { + return StyleSheet.create({ + container: { + flex: 1, + backgroundColor: theme.color.background, + }, + content: { + flex: 1, + padding: theme.spacing.m, + }, + imageContainer: { + position: 'relative', + }, + badgeImageBackgroundContainer: { + width: '100%', + alignItems: 'center', + justifyContent: 'center', + }, + badgeImageBackground: { + borderRadius: theme.borderRadius.l, + borderWidth: 1, + borderColor: theme.color.lightGrey, + }, + badgeImage: { + height: 308, + width: 264, + margin: 18, + }, + addressSection: { + marginVertical: theme.spacing.l, + }, + alertSection: { + marginBottom: theme.spacing.l, + }, + transactionContainer: { + borderColor: theme.color.lightGrey, + borderTopWidth: 1, + shadowOpacity: 1, + shadowRadius: 4, + shadowOffset: { height: 4, width: 0 }, + shadowColor: `0px 4px 24px rgba(0, 0, 0, 0.04)`, + paddingTop: 12, + paddingBottom: 16, + paddingHorizontal: 16, + }, + transactionContent: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 12, + }, + trxnText: { + fontFamily: NUNITO_SANS, + fontWeight: '600', + fontSize: theme.fontSize.m, + color: theme.color.grey400, + }, + addressTitle: { + fontFamily: NUNITO_SANS, + fontWeight: '600', + fontSize: theme.fontSize.m, + color: theme.color.onBackground, + marginBottom: theme.spacing.s, + }, + title: { + fontFamily: NUNITO_SANS_SEMIBOLD, + fontWeight: '700', + fontSize: 22, + textAlign: 'justify', + color: theme.color.onBackground, + marginTop: theme.spacing.l, + marginBottom: theme.spacing.s, + }, + bodyText: { + fontFamily: NUNITO_SANS, + fontWeight: '600', + fontSize: theme.fontSize.s, + color: theme.color.onBackground, + marginBottom: theme.spacing.m, + }, + addressListItem: { + elevation: 4, + borderColor: theme.color.lightGrey, + borderWidth: 1, + shadowOpacity: 1, + shadowRadius: theme.borderRadius.xs, + shadowOffset: { height: 4, width: 0 }, + shadowColor: `0px 4px 24px rgba(0, 0, 0, 0.04)`, + }, + actionButton: { + fontSize: theme.fontSize.s, + paddingHorizontal: theme.spacing.sm, + paddingVertical: theme.spacing.xs, + }, + addressList: { + marginTop: theme.spacing.l, + }, + }) +} diff --git a/src/pages/ClaimBadges/ClaimBadgeStatus.tsx b/src/pages/ClaimBadges/ClaimBadgeStatus.tsx new file mode 100644 index 000000000..4399fb828 --- /dev/null +++ b/src/pages/ClaimBadges/ClaimBadgeStatus.tsx @@ -0,0 +1,149 @@ +import { VeridaBadge } from 'features/badges/@types' +import React from 'react' +import { Image, ImageBackground, StyleSheet, Text, View } from 'react-native' + +import ErrorStatusIcon from 'assets/icons/error_status_icon.svg' +import Button from 'components/Button' +import { Paragraph } from 'components/Typography/Paragraph' +import { NUNITO_SANS, NUNITO_SANS_SEMIBOLD } from 'constants/text' +import { useThemeAwareStyle } from 'hooks/useThemeAwareStyle' +import { Theme } from 'styles/types' + +const badgeImageBackground = require('assets/badge_gradient_bg.png') + +const statusList = { + success: { + type: 'success', + title: `Success!`, + message: (badgeLabel: string) => + `Your ${badgeLabel} Badge has been successfully generated`, + }, + error: { + type: 'error', + title: `Ooops...`, + message: `Something went wrong +Please try again`, + }, +} + +type ClaimBadgeStatusProps = { + status: keyof typeof statusList + badgeInfo: VeridaBadge +} + +const ClaimBadgeStatus: React.FC = ({ + status, + badgeInfo, +}) => { + const styles = useThemeAwareStyle(createStyles) + + // TODO: Set actions to the buttons + const actions = + status === 'success' ? ( + + + + + ) : ( + + ) + + const message: string = + status === 'success' + ? statusList.success.message(badgeInfo.label) + : statusList.error.message + + // TODO: Add animations as designed in Figma + return ( + + + {status === 'error' && } + {status === 'success' && ( + + + + )} + + {statusList[status].title} + {message} + + + {actions} + + ) +} + +export default ClaimBadgeStatus + +// TODO: Rework the sizing of the image. Maybe create a dedicated component +const createStyles = (theme: Theme) => { + return StyleSheet.create({ + container: { + flex: 1, + flexDirection: 'column', + justifyContent: 'space-between', + }, + content: { + justifyContent: 'center', + alignItems: 'center', + marginTop: 64, + paddingHorizontal: theme.spacing.m, + }, + badgeImageBackgroundContainer: { + width: 221, + height: 221, + alignItems: 'center', + justifyContent: 'center', + }, + badgeImageBackground: { + borderRadius: theme.borderRadius.l, + borderWidth: 1, + borderColor: theme.color.lightGrey, + }, + badgeImage: { + width: 170, + height: 198, + margin: 18, + }, + statusInfoContainer: { + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + }, + statusTitle: { + fontFamily: NUNITO_SANS_SEMIBOLD, + fontWeight: '700', + fontSize: 28, + lineHeight: 36.4, + textAlign: 'center', + color: theme.color.onBackground, + marginTop: theme.spacing.l, + marginBottom: theme.spacing.m, + }, + statusMessage: { + fontFamily: NUNITO_SANS, + fontSize: theme.fontSize.l, + lineHeight: 24, + textAlign: 'center', + color: theme.color.onBackground, + opacity: 0.6, + }, + }) +} diff --git a/src/pages/ClaimBadges/ClaimableBadges.tsx b/src/pages/ClaimBadges/ClaimableBadges.tsx new file mode 100644 index 000000000..9034cefd9 --- /dev/null +++ b/src/pages/ClaimBadges/ClaimableBadges.tsx @@ -0,0 +1,245 @@ +import { useNavigation } from '@react-navigation/native' +import * as Sentry from '@sentry/react-native' +import { VeridaBadge } from 'features/badges/@types' +import { BadgeList } from 'features/badges/components' +import React, { useEffect, useState } from 'react' +import { Linking, SafeAreaView, StyleSheet, Text, View } from 'react-native' +import FastImage from 'react-native-fast-image' + +import DataConnectorsManager from 'api/DataConnectorsManager' +import { SBTManager } from 'api/SBTManager' +import AppAlert from 'components/AppAlert/AppAlert' +import Button from 'components/Button' +import AppModal from 'components/modal/AppModal' +import NavigationHeader from 'components/Navigation/NavigationHeader' +import { Headline } from 'components/Typography/Headline' +import { Paragraph } from 'components/Typography/Paragraph' +import { TEXT_COLOR } from 'constants/color' +import { NUNITO_SANS, NUNITO_SANS_SEMIBOLD } from 'constants/text' +import { VERIDA_ONE_FAQ_URL } from 'constants/url' +import { useThemeAwareStyle } from 'hooks/useThemeAwareStyle' +import { Theme } from 'styles/types' + +const ClaimableBadges: React.FC = () => { + const styles = useThemeAwareStyle(createStyles) + const navigation = useNavigation() + const [infoModalVisible, setInfoModalVisible] = useState(false) + const [availableBadges, setAvailableBadges] = useState([]) + const [loading, setLoading] = useState(true) + + const [supportedConnectPlatforms, setSupportedConnectPlatforms] = useState< + any[] + >([]) + + const handleWhatIsVeridaBadgesInfoPress = () => { + setInfoModalVisible(true) + } + + const handleWhatIsVeridaBadgesModalClose = () => { + setInfoModalVisible(false) + } + + const handleWhatIsVeridaBadgesReadMoreLinkPress = () => { + Linking.openURL(VERIDA_ONE_FAQ_URL) + } + + const whatIsVeridaBadgesModalFooter = ( + + ) + + useEffect(() => { + const init = async () => { + const allAvailableBadges = + await SBTManager.getInstance().getAvailableBadges() + console.log('Badges', JSON.stringify(allAvailableBadges, null, 2)) + setAvailableBadges(allAvailableBadges) + } + + init() + }, []) + + useEffect(() => { + function buildConnections(allConnectors: any) { + const finalConnectors = [] + for (const connectorName in allConnectors) { + finalConnectors.push(allConnectors[connectorName].render()) + } + + return finalConnectors + } + + const fetchPlatformConnections = async () => { + try { + setLoading(true) + DataConnectorsManager.triggerSync() + + const currentConnectors = await DataConnectorsManager.getConnectors() + setSupportedConnectPlatforms(buildConnections(currentConnectors)) + } catch (error) { + Sentry.captureException(error) + } + } + + fetchPlatformConnections() + + const onConnectionUpdated = async () => { + // Connection has been updated, so update UI + const conns = await DataConnectorsManager.getConnectors() + setSupportedConnectPlatforms(buildConnections(conns)) + } + DataConnectorsManager.on('connectionUpdated', onConnectionUpdated) + const onLogout = async () => { + await DataConnectorsManager.resetConnector() + } + DataConnectorsManager.on('logout', onLogout) + return () => { + DataConnectorsManager.off('connectionUpdated', onConnectionUpdated) + DataConnectorsManager.off('logout', onLogout) + } + }, []) + + return ( + + + + Verida Badges + + Connect your social accounts to verify and claim your Verida Badges. + They will appear on your Verida One profile and enable dApps to verify + your identity. + + + + + + Available Badges + + + + + Connect to get more Badges + {supportedConnectPlatforms.map((platform) => ( + + + + {platform.label} + + + + + ))} + + + + + + Verida One is your web3 native public profile to showcase your web2 + and web3 identities. It provides a single source of truth about + activities and ownership that can be read by both humans (via a web + UI) and programs (via methods including on-chain records and Verida + APIs for off-chain data). Verida Badges represent a verified proof + of ownership of a web2 platform account that you control. Verida + badges are on-chain Soulbound Tokens which are a tokenised + attestation of your ownership claim. You can mint Verida Badges from + the Verida Vault and have them displayed on your public Verida One + profile. By issuing verified Badges, Verida acts as a trusted + authority recognised by users, communities and dapps. Soulbound + Tokens (SBTs) are a new cryptographic primitive that attests and + graphs reputational value in a native blockchain environment. SBTs + are a kind of non-transferable asset, often referred to as a + Non-Transferable NFT. + + + + + ) +} + +export default ClaimableBadges + +const createStyles = (theme: Theme) => { + return StyleSheet.create({ + container: { + flex: 1, + backgroundColor: theme.color.background, + }, + content: { + paddingHorizontal: theme.spacing.m, + }, + headline: { + fontFamily: NUNITO_SANS_SEMIBOLD, + fontWeight: '600', + fontSize: 28, + color: TEXT_COLOR, + marginTop: theme.spacing.l, + marginBottom: theme.spacing.s, + }, + modalContent: { + paddingHorizontal: theme.spacing.m, + }, + bodyText: { + fontFamily: NUNITO_SANS, + fontWeight: '400', + fontSize: theme.fontSize.m, + }, + alertContainer: { + marginTop: theme.spacing.m, + }, + listSection: { + marginTop: theme.spacing.xxl, + }, + listTitle: { + fontFamily: NUNITO_SANS_SEMIBOLD, + fontWeight: '700', + fontSize: 17, + color: TEXT_COLOR, + marginBottom: theme.spacing.s, + }, + + connectionItem: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 12, + }, + connectionItemIconLabel: { + flexDirection: 'row', + alignItems: 'center', + }, + itemIcon: { width: 48, height: 48, borderRadius: 24, marginRight: 10 }, + itemText: { + fontSize: 18, + }, + }) +} diff --git a/src/pages/Connections/DataConnector.js b/src/pages/Connections/DataConnector.js index ab72cc979..f17d300d8 100644 --- a/src/pages/Connections/DataConnector.js +++ b/src/pages/Connections/DataConnector.js @@ -7,6 +7,7 @@ import { TouchableOpacity, View, } from 'react-native' +import FastImage from 'react-native-fast-image' import DataConnectorsManager from 'api/DataConnectorsManager' import NavigationHeader from 'components/Navigation/NavigationHeader' @@ -64,12 +65,15 @@ export default (props) => { { props.navigation.navigate('SingleConnection', { - provider: item.name, + provider: item, }) }} style={styles.connectionItem}> - + {item.label} {item.syncStatus} diff --git a/src/pages/Connections/SingleConnection.js b/src/pages/Connections/SingleConnection.js index f648f098b..74f1f27c6 100644 --- a/src/pages/Connections/SingleConnection.js +++ b/src/pages/Connections/SingleConnection.js @@ -1,6 +1,7 @@ import { Container, Content, Icon } from 'native-base' import React, { useEffect, useState } from 'react' import { Image, StyleSheet, View } from 'react-native' +import FastImage from 'react-native-fast-image' import DataConnectorsManager from 'api/DataConnectorsManager' import Button from 'components/Button' @@ -22,9 +23,9 @@ const calculateNextSync = function (conn) { } export default ({ route, navigation }) => { - const provider = route.params.provider + const connectionInfo = route.params.provider + const provider = connectionInfo.name const connectNow = route.params.connectNow - const connectionInfo = DataConnectorsManager.getConnectionInfo(provider) const [syncStatus, setSyncStatus] = useState('') const [nextSync, setNextSync] = useState('') @@ -117,7 +118,10 @@ export default ({ route, navigation }) => { )} - + {syncStatus === 'disabled' ? ( + + + + {/* * */} + {!isEmpty(credentialPresentationUri) ? ( diff --git a/src/pages/Wallets/ManageWallets.tsx b/src/pages/Wallets/ManageWallets.tsx index 21224d92c..6545cfe6d 100644 --- a/src/pages/Wallets/ManageWallets.tsx +++ b/src/pages/Wallets/ManageWallets.tsx @@ -6,12 +6,17 @@ import React, { useEffect, useState } from 'react' import { Alert, StyleSheet, View } from 'react-native' import { connect } from 'react-redux' import { Dispatch } from 'redux' +import { WalletItem } from 'types/wallet' +import PlusIcon from 'assets/plus_icon.svg' +import UnionIcon from 'assets/union_icon.svg' import LoadingView from 'components/LoadingView' import NavigationHeader from 'components/Navigation/NavigationHeader' import WalletList from 'components/WalletList' -import { WalletItem } from 'components/WalletList/types' +import CreateWalletModal from 'components/WalletModal/AddWalletModal' +import ImportWalletModal from 'components/WalletModal/ImportWalletModal' import CONFIG from 'config/environment' +import { BLACK_COLOR, SNOW_COLOR } from 'constants/color' import { MainStackParams } from 'navigation/types' import { selectChains } from 'reduxStore/tokens/selectors' import { @@ -28,12 +33,7 @@ import { getWalletProcessingState, } from 'reduxStore/wallet/selectors' -import PlusIcon from '../../assets/plus_icon.svg' -import UnionIcon from '../../assets/union_icon.svg' -import { BLACK_COLOR, SNOW_COLOR } from '../../constants/color' -import CreateWalletModal from './AddWalletModal' import { AddWatchedWalletModal } from './AddWatchedWalletModal' -import ImportWalletModal from './ImportWalletModal' export type walletIdType = string diff --git a/src/pages/Wallets/SingleWallet.tsx b/src/pages/Wallets/SingleWallet.tsx index d97314aa6..a32618851 100644 --- a/src/pages/Wallets/SingleWallet.tsx +++ b/src/pages/Wallets/SingleWallet.tsx @@ -137,7 +137,7 @@ const SingleWallet = (props: Props) => { Seed phrase )} - {isChainTypeEvm && singleWallet.privateKey && ( + {singleWallet.privateKey && ( showPrivateKey(singleWallet.privateKey)} style={styles.actionButton}> diff --git a/src/reduxStore/wallet/selectors.js b/src/reduxStore/wallet/selectors.js index 036cae2bf..0ac2a4257 100644 --- a/src/reduxStore/wallet/selectors.js +++ b/src/reduxStore/wallet/selectors.js @@ -153,6 +153,30 @@ export const getWalletList = (state) => { }) } +export const getAddressList = (state, allChains, blockchainNetwork) => { + const allWallets = getAllWallets(state) + return Object.values(allWallets) + .filter((wallet) => Boolean(wallet.mnemonic)) // filter out watched wallets + .map((wallet) => { + const { id, label } = wallet + const addresses = Object.keys(wallet.accounts) + .map((key) => { + return { + blockchainNetwork: key, + address: wallet.accounts[key].address, + } + }) + .filter((item) => item.blockchainNetwork === blockchainNetwork) + return { + id, + label, + icon: allChains[blockchainNetwork].icon, + count: Object.keys(wallet.accounts).length, + address: addresses[0]?.address, + } + }) +} + export const getUniqueWalletAddresses = (wallet) => { if (!wallet) return [] @@ -173,6 +197,13 @@ export const getSelectedWalletById = (state) => { return selectedWallet } +export const getSelectedAddressById = (state, chains, network) => { + const walletList = getAddressList(state, chains, network) + const selectedWalletId = state.selectedWallet + const selectedWallet = walletList.find((item) => item.id === selectedWalletId) + return selectedWallet +} + export const getWalletProcessingState = (state) => { return state.walletProcessing.loading } diff --git a/src/styles/button.js b/src/styles/button.js index 5ae9ef1c6..919f7ed96 100644 --- a/src/styles/button.js +++ b/src/styles/button.js @@ -4,9 +4,11 @@ import { DISABLED_COLOR, LIGHTGREY_COLOR, PRIMARY_COLOR, + PRIMARY_COLOR_200, + PRIMARY_COLOR_300, WHITE_COLOR, -} from '../constants/color' -import { NUNITO_SANS_BOLD } from '../constants/text' +} from '/constants/color' +import { NUNITO_SANS_BOLD } from '/constants/text' const transparent = { backgroundColor: 'transparent', @@ -31,13 +33,19 @@ export default StyleSheet.create({ backgroundColor: 'transparent', borderColor: WHITE_COLOR, }, + 'light-primary': { + backgroundColor: PRIMARY_COLOR_200, + color: PRIMARY_COLOR_300, + borderWidth: 0, + }, primary: { backgroundColor: PRIMARY_COLOR, borderColor: PRIMARY_COLOR, }, secondary: { backgroundColor: WHITE_COLOR, - borderColor: WHITE_COLOR, + borderColor: LIGHTGREY_COLOR, + borderWidth: 1, }, warning: { backgroundColor: WHITE_COLOR, diff --git a/src/styles/theme.ts b/src/styles/theme.ts index 9439b7aa3..b87f98c06 100644 --- a/src/styles/theme.ts +++ b/src/styles/theme.ts @@ -1,6 +1,7 @@ import Color from 'color' import { + ALERT_INFO_COLOR, BACKGROUND_GREY_COLOR, BLACK_COLOR_OPACITY, DECLINE_COLOR, @@ -39,6 +40,7 @@ export const defaultTheme = { onDarkBackground: WHITE_COLOR, error: DECLINE_COLOR, + info: ALERT_INFO_COLOR, onError: WHITE_COLOR, success: SUCCESS_COLOR, onSuccess: WHITE_COLOR, @@ -122,4 +124,11 @@ export const defaultTheme = { s: 16, m: 24, }, + borderRadius: { + xs: 4, + s: 8, + sm: 12, + m: 16, + l: 24, + }, } diff --git a/src/types/wallet.ts b/src/types/wallet.ts index 24be54c19..a95847ab1 100644 --- a/src/types/wallet.ts +++ b/src/types/wallet.ts @@ -1,3 +1,32 @@ +export type SingleAccountType = { + mnemonic: string + privateKey: string + publicKey: string + address: string +} + +export type AccountsType = { + [key: string]: SingleAccountType +} + +export type WalletType = { + id: string + type: string + seedPhrase: string + label: string + accounts: [AccountsType] + chain?: string +} + +export type WalletItem = { + count: number + icon: string + id: string + label: string + other?: any + address?: string +} + export type VeridaWalletType = 'single' | 'multi' export type CaipWalletType = 'eip155' | 'algorand' | 'near' diff --git a/tsconfig.json b/tsconfig.json index d50a57198..cfc102453 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -52,6 +52,9 @@ ], "utils/*": [ "src/utils/*" + ], + "types/*": [ + "src/types/*" ] } }, diff --git a/yarn.lock b/yarn.lock index b5569bed0..68e2ef385 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5523,6 +5523,17 @@ axios "^0.27.2" ethers "^5.7.0" +"@verida/vda-sbt-client@^2.2.1": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@verida/vda-sbt-client/-/vda-sbt-client-2.3.5.tgz#361f2030bd4a15895bccb465518993a5190ed919" + integrity sha512-kwF6NrRtcBGMXWNzleobMM4YrCvAfTtQSsMlSKGiB8Ka3BAlPwuOmqJo1Hw1qe/hRN9CJkvYCBqFwUE84H2iSg== + dependencies: + "@ethersproject/providers" "^5.7.2" + "@verida/helpers" "^2.3.1" + "@verida/web3" "^2.3.4" + axios "^0.27.2" + ethers "^5.7.0" + "@verida/verifiable-credentials@^2.3.4": version "2.3.4" resolved "https://registry.yarnpkg.com/@verida/verifiable-credentials/-/verifiable-credentials-2.3.4.tgz#1d6f23d759b19000a8b863af364980136db19864"