diff --git a/docs/build/guides/account-linking-with-dapper.md b/docs/build/guides/account-linking-with-dapper.md new file mode 100644 index 0000000000..7716057365 --- /dev/null +++ b/docs/build/guides/account-linking-with-dapper.md @@ -0,0 +1,752 @@ +--- +title: Account Linking With NBA Top Shot +description: Use Account Linking between the Dapper Wallet and Flow Wallet to effortlessly use NBA Top Shot Moments in your app. +sidebar_position: 5 +sidebar_custom_props: + icon: ⛓️ +--- + +# Account Linking With NBA Top Shot + +[Account Linking] is a powerful Flow feature that allows users to connect their wallets, enabling linked wallets to view and manage assets in one wallet with another. This feature helps reduce or even eliminate the challenges posed by other account abstraction solutions, which often lead to multiple isolated wallets and fragmented assets. + +In this tutorial, you'll build a [simple onchain app] that allows users to sign into your app with their Flow wallet and view [NBA Top Shot] Moments that reside in their [Dapper Wallet] - without those users needing to sign in with Dapper. + +## Objectives + +After completing this guide, you'll be able to: + +* Pull your users' NBA Top Shot Moments into your Flow app without needing to transfer them out of their Dapper wallet +* Retrieve and list all NFT collections in any child wallet linked to a given Flow address +* Write a [Cadence] script to iterate through the storage of a Flow wallet to find NFT collections +* Run Cadence Scripts from the frontend + +## Prerequisites + +### Next.js and Modern Frontend Development + +This tutorial uses [Next.js]. You don't need to be an expert, but it's helpful to be comfortable with development using a current React framework. You'll be on your own to select and use a package manager, manage Node versions, and other frontend environment tasks. If you don't have your own preference, you can just follow along with us and use [Yarn]. + +### Flow Wallet + +You'll need a [Flow Wallet], but you don't need to deposit any funds. + +## Moments NFTs + +You'll need a [Dapper Wallet] containing some Moments NFTs, such as [NBA Top Shot] Moments. + +## Getting Started + +This tutorial will use a [Next.js] project as the foundation of the frontend. Create a new project with: + +```zsh +npx create-next-app@latest +``` + +We will be using TypeScript and the App Router, in this tutorial. + +Open your new project in the editor of your choice, install dependencies, and run the project. + +```zsh +yarn install +yarn run dev +``` + +If everything is working properly, you'll be able to navigate to `localhost:3000` and see the default [Next.js] page. + +## Flow Cadence Setup + +You'll need a few more dependencies to efficiently work with Cadence inside of your app. + +### Flow CLI and Types + +The [Flow CLI] contains a number of command-line tools for interacting with the Flow ecosystem. If you don't already have it installed, you can add it with Brew (or using [other installation methods]): + +```zsh +brew install flow-cli +``` + +Once it's installed, you'll need to initialize Flow in your Next.js project. From the root, run: + +```zsh +flow init --config-only +``` + +The `--config-only` flag [initializes a project] with the just the config file. This allows the Flow CLI to interact with your project without adding any unnecessary files. + +Next, you'll need to do a little bit of config work so that your project knows how to read Cadence files. Install the Flow Cadence Plugin: + +```zsh +yarn add flow-cadence-plugin --dev +``` + +Finally, open `next.config.ts` and update it to use the plugin with Raw Loader: + +```tsx +// next.config.ts +import type { NextConfig } from "next"; +import FlowCadencePlugin from "flow-cadence-plugin"; + +const nextConfig: NextConfig = { + webpack: (config) => { + config.plugins.push(new FlowCadencePlugin()) + + return config; + }, +}; + +export default nextConfig; +``` + +## Frontend Setup + +We'll use the Flow Client Library [FCL] to manage blockchain interaction from the frontend. It's similar to viem, ethers, or web3.js, but works with the Flow blockchain and transactions and scripts written in Cadence. + +```zsh +yarn add @onflow/fcl +``` + +Go ahead and install `dotenv` as well: + +``` +yarn add dotenv +``` + +### Provider Setup + +A fair amount of boilerplate code is needed to set up your provider. We'll provide it, but since it's not the purpose of this tutorial, we'll be brief on explanations. For more details, check out the [App Quickstart Guide]. + +Add `app/providers/AuthProvider.tsx`: + +```tsx +'use client'; +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { createContext, useContext, ReactNode } from 'react'; +import useCurrentUser from '../hooks/use-current-user.hook'; + +interface State { + user: any; + loggedIn: any; + logIn: any; + logOut: any; +} + +const AuthContext = createContext(undefined); + +interface AuthProviderProps { + children: ReactNode; +} + +const AuthProvider: React.FC = ({ children }) => { + const [user, loggedIn, logIn, logOut] = useCurrentUser(); + + return ( + + {children} + + ); +}; + +export default AuthProvider; + +export const useAuth = (): State => { + const context = useContext(AuthContext); + + if (context === undefined) { + throw new Error('useAuth must be used within a AuthProvider'); + } + + return context; +}; +``` + +Then, add `app/hooks/use-current-user-hook.tsx`: + +```tsx +import { useEffect, useState } from 'react'; +import * as fcl from '@onflow/fcl'; + +export default function useCurrentUser() { + const [user, setUser] = useState({ addr: null }); + + const logIn = () => { + fcl.authenticate(); + }; + + const logOut = () => { + fcl.unauthenticate(); + }; + + useEffect(() => { + fcl.currentUser().subscribe(setUser); + }, []); + + return {user, loggedIn: user?.addr != null, logIn, logOut}; +} +``` + +## .env + +Add a `.env` to the root and fill it with: + +```text +NEXT_PUBLIC_ACCESS_NODE_API="https://rest-mainnet.onflow.org" +NEXT_PUBLIC_FLOW_NETWORK="mainnet" +NEXT_PUBLIC_WALLETCONNECT_ID= +``` + +:::warning + +Don't forget to replace `` with your own [Wallet Connect] app id! + +::: + +### Implement the Provider and Flow Config + +Finally, open `layout.tsx`. Start by importing Flow dependencies and the AuthProvider: + +```tsx +import flowJSON from '../flow.json' +import * as fcl from "@onflow/fcl"; + +import AuthProvider from "./providers/AuthProvider"; +``` + +Then add your Flow config: + +```tsx +fcl.config({ + "discovery.wallet": "https://fcl-discovery.onflow.org/authn", + 'accessNode.api': process.env.NEXT_PUBLIC_ACCESS_NODE_API, + 'flow.network': process.env.NEXT_PUBLIC_FLOW_NETWORK, + 'walletconnect.projectId': process.env.NEXT_PUBLIC_WALLETCONNECT_ID +}).load({ flowJSON }); +``` + +:::warning + +We're going to force some things client side to get this proof-of-concept working quickly. Use Next.js best practices for a production app. + +::: + +Add a `'use client';` directive to the top of the file and **delete** the import for Metadata and fonts, as well as the code related to them. + +Finally, update the `` to remove the font references and suppress hydration warnings: + +```tsx + +``` + +Your code should be: + +```tsx +// layout.tsx +'use client'; +import "./globals.css"; +import flowJSON from '../flow.json' +import * as fcl from "@onflow/fcl"; + +import AuthProvider from "./providers/AuthProvider"; + +fcl.config({ + "discovery.wallet": "https://fcl-discovery.onflow.org/authn", + 'accessNode.api': process.env.NEXT_PUBLIC_ACCESS_NODE_API, + 'flow.network': process.env.NEXT_PUBLIC_FLOW_NETWORK, + 'walletconnect.projectId': process.env.NEXT_PUBLIC_WALLETCONNECT_ID +}).load({ flowJSON }); + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + {children} + + + + ); +} +``` + +### Add the Connect Button + +Open `page.tsx` and clean up the demo code leaving only the `
` block: + +```tsx +import Image from "next/image"; + +export default function Home() { + return ( +
+
+
TODO
+
+
+ ); +} +``` + +Add a `'use client';` directive, import the `useAuth` hook and instantiate it in the `Home` function: + +```tsx +'use client'; +import { useAuth } from "./providers/AuthProvider"; +``` + +```tsx +const { user, loggedIn, logIn, logOut } = useAuth(); +``` + +Then add a button in the `
` to handle logging in or out: + +```tsx +
+
Welcome
+ +
+``` + +## Testing Pass + +Run the app: + +```zsh +yarn dev +``` + +You'll see your `Log In` button in the middle of the window. + +![Welcome](welcome.png) + +Click the button and log in with your Flow wallet. + +![Flow Wallet](flow-wallet.png) + +## Account Linking + +Now that your app is set up, you can make use of [Account Linking] to to pull your NFTs from your Dapper Wallet, through your Flow Wallet, and into the app. + +### Setting Up Account Linking + +If you haven't yet, you'll need to [link your Dapper Wallet] to your Flow Wallet. + +:::warning + +The Dapper Wallet requires that you complete KYC before you can use Account Linking. While this may frustrate some members of the community, it makes it much easier for app developers to design onboarding rewards and bonuses that are less farmable. + +::: + +### Discovering the NFTs with a Script + +With your accounts linked, your Flow Wallet now has a set of capabilities related to your Dapper Wallet and it's permitted to use those to view and even manipulate those NFTs and assets. + +Before you can add a script that can handle this, you'll need to import the `HybridCustody` contract using the [Flow Dependency Manager]: + +```zsh +flow dependencies add mainnet://d8a7e05a7ac670c0.HybridCustody +``` + +Choose `none` to skip deploying on the `emulator` and skip adding testnet aliases. There's no point, these NFTs are on mainnet! + +You'll get a complete summary from the Dependency Manager: + +```zsh +📝 Dependency Manager Actions Summary + +🗃️ File System Actions: +✅️ Contract HybridCustody from d8a7e05a7ac670c0 on mainnet installed +✅️ Contract MetadataViews from 1d7e57aa55817448 on mainnet installed +✅️ Contract FungibleToken from f233dcee88fe0abe on mainnet installed +✅️ Contract ViewResolver from 1d7e57aa55817448 on mainnet installed +✅️ Contract Burner from f233dcee88fe0abe on mainnet installed +✅️ Contract NonFungibleToken from 1d7e57aa55817448 on mainnet installed +✅️ Contract CapabilityFactory from d8a7e05a7ac670c0 on mainnet installed +✅️ Contract CapabilityDelegator from d8a7e05a7ac670c0 on mainnet installed +✅️ Contract CapabilityFilter from d8a7e05a7ac670c0 on mainnet installed + +💾 State Updates: +✅ HybridCustody added to emulator deployments +✅ Alias added for HybridCustody on mainnet +✅ HybridCustody added to flow.json +✅ MetadataViews added to flow.json +✅ FungibleToken added to flow.json +✅ ViewResolver added to flow.json +✅ Burner added to flow.json +✅ NonFungibleToken added to flow.json +✅ CapabilityFactory added to emulator deployments +✅ Alias added for CapabilityFactory on mainnet +✅ CapabilityFactory added to flow.json +✅ CapabilityDelegator added to emulator deployments +✅ Alias added for CapabilityDelegator on mainnet +✅ CapabilityDelegator added to flow.json +✅ CapabilityFilter added to emulator deployments +✅ Alias added for CapabilityFilter on mainnet +✅ CapabilityFilter added to flow.json +``` + +Add `app/cadence/scripts/FetchNFTsFromLinkedAccts.cdc`. In it, add this script. Review the inline comments to see what each step is doing: + +```cadence +import "HybridCustody" +import "NonFungibleToken" +import "MetadataViews" + +// This script iterates through a parent's child accounts, +// identifies private paths with an accessible NonFungibleToken.Provider, and returns the corresponding typeIds + +access(all) fun main(addr: Address): AnyStruct { + let manager = getAuthAccount(addr).storage.borrow(from: HybridCustody.ManagerStoragePath) + ?? panic ("manager does not exist") + + var typeIdsWithProvider: {Address: [String]} = {} + var nftViews: {Address: {UInt64: MetadataViews.Display}} = {} + + let providerType = Type() + let collectionType: Type = Type<@{NonFungibleToken.CollectionPublic}>() + + for address in manager.getChildAddresses() { + let acct = getAuthAccount(address) + let foundTypes: [String] = [] + let views: {UInt64: MetadataViews.Display} = {} + let childAcct = manager.borrowAccount(addr: address) ?? panic("child account not found") + + // Iterate through storage paths to find NFTs that are controlled by the parent account + // To just find NFTs, check if thing stored is nft collection and borrow it as NFT collection and get IDs + for s in acct.storage.storagePaths { + // Iterate through capabilities + for c in acct.capabilities.storage.getControllers(forPath: s) { + if !c.borrowType.isSubtype(of: providerType){ + // If this doen't have providerType, it's not an NFT collection + continue + } + + // We're dealing with a Collection but we need to check if accessible from the parent account + if let cap: Capability = childAcct.getCapability(controllerID: c.capabilityID, type: providerType) { // Part 1 + let providerCap = cap as! Capability<&{NonFungibleToken.Provider}> + + if !providerCap.check(){ + // If I don't have access to control the account, skip it. + // Disable this check to do something else. + // + continue + } + + foundTypes.append(cap.borrow<&AnyResource>()!.getType().identifier) + typeIdsWithProvider[address] = foundTypes + // Don't need to keep looking at capabilities, we can control NFT from parent account + break + } + } + } + + // Iterate storage, check if typeIdsWithProvider contains the typeId, if so, add to views + acct.storage.forEachStored(fun (path: StoragePath, type: Type): Bool { + + if typeIdsWithProvider[address] == nil { + return true + } + + for key in typeIdsWithProvider.keys { + for idx, value in typeIdsWithProvider[key]! { + let value = typeIdsWithProvider[key]! + + if value[idx] != type.identifier { + continue + } else { + if type.isInstance(collectionType) { + continue + } + if let collection = acct.storage.borrow<&{NonFungibleToken.CollectionPublic}>(from: path) { + // Iterate over IDs & resolve the Display view + for id in collection.getIDs() { + let nft = collection.borrowNFT(id)! + if let display = nft.resolveView(Type())! as? MetadataViews.Display { + views.insert(key: id, display) + } + } + } + continue + } + } + } + return true + }) + nftViews[address] = views + } + return nftViews +} +``` + +:::warning + +The above script is a relatively naive implementation. For production, you'll want to filter for only the collections you care about, and you will eventually need to add handling for very large collections in a wallet. + +::: + +### Running the Script and Displaying the NFTs + +Add a component in `app/components` called `DisplayLinkedNFTs.cdc`. + +In it, import dependencies from React and FCL, as well as the script you just added: + +```tsx +import React, { useState, useEffect } from 'react'; +import * as fcl from "@onflow/fcl"; +import * as t from '@onflow/types'; + +import FetchNFTs from '../cadence/scripts/FetchNFTsFromLinkedAccts.cdc'; +``` + +As we're using TypeScript, you should add some types as well to manage the data from the NFTs nicely. For now, just add them to this file: + +```typescript +type Thumbnail = { + url: string; +}; + +type Moment = { + name: string; + description: string; + thumbnail: Thumbnail; +}; + +type MomentsData = { + [momentId: string]: Moment; +}; + +type ApiResponse = { + [address: string]: MomentsData; +}; + +interface AddressDisplayProps { + address: string; +} +``` + +Then, add the function for the component: + +```tsx +const DisplayLinkedNFTs: React.FC = ({ address }) => { + // TODO... + + return ( +
Nothing here yet
+ ) +} + +export default DisplayLinkedNFTs; +``` + +In the function, add a state variable to store the data retrieved by the script: + +```typescript +const [responseData, setResponseData] = useState(null); +``` + +Then, use `useEffect` to fetch the NFTs with the script and `fcl.query`: + +```tsx +useEffect(() => { + const fetchLinkedAddresses = async () => { + if (!address) return; + + try { + const cadenceScript = FetchNFTs; + + // Fetch the linked addresses + const response: ApiResponse = await fcl.query({ + cadence: cadenceScript, + args: () => [fcl.arg(address, t.Address)], + }); + + console.log(JSON.stringify(response, null, 2)); + + setResponseData(response); + } catch (error) { + console.error("Error fetching linked addresses:", error); + } + }; + + fetchLinkedAddresses(); +}, [address]); +``` + +Return to `page.tsx`, import your new component, and add an instance of `` that passes in the user's address and is only displayed while `loggedIn`. + +```tsx +{loggedIn && } +``` + +### Testing + +Run the app again. If you have linked your account and have NFTs in that account, you'll see them in the console! + +### Displaying the Moments + +Now that they're here, all to do is display them nicely! Return to `DisplayLinkedNFTs.tsx`. Add a helper function to confirm each returned NFT matches the Moments format. You can update this to handle other NFTs you'd like to show as well. + +:::warning + +Remember, you'll also need to update the script in a production app to filter for only the collections you want, and handle large collections. + +::: + +```tsx +// Type-checking function to validate moment structure +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const isValidMoment = (moment: any): moment is Moment => { + const isValid = + typeof moment.name === 'string' && + typeof moment.description === 'string' && + moment.thumbnail && + typeof moment.thumbnail.url === 'string'; + + if (!isValid) { + console.warn('Invalid moment data:', moment); + } + + return isValid; +}; +``` + +Next, add a rendering function with some basic styling: + +```tsx +// Function to render moments with validation +const renderMoments = (data: ApiResponse) => { + return Object.entries(data).map(([addr, moments]) => ( +
+

Linked Wallet: {addr}

+
+ {Object.entries(moments).map(([momentId, moment]) => ( + isValidMoment(moment) ? ( +
+
{moment.name}
+

{moment.description}

+ {moment.name} +
+ ) : null + ))} +
+
+ )); +}; +``` + +Finally, update the `return` with some more styling and the rendered NFT data: + +```tsx +return ( +
+ {address ? ( +
+

Moments Data:

+
+ {responseData ? renderMoments(responseData) : ( +

No Moments Data Available

+ )} +
+
+ ) : ( +
No Address Provided
+ )} +
+); +``` + +### Further Polish + +Finally, you can polish up your `page.tsx` to look a little nicer, and guide your users to the Account Linking process in the Dapper Wallet: + +```tsx +'use client'; +import DisplayLinkedNFTs from "./components/DisplayLinkedNFTs"; +import { useAuth } from "./providers/AuthProvider"; + +export default function Home() { + const { user, loggedIn, logIn, logOut } = useAuth(); + + return ( +
+
+ {/* Message visible for all users */} +

+ Please link your Dapper wallet to view your NFTs. For more information, check the{" "} + + Account Linking and FAQ + . +

+ +
+ {/* Display user address or linked NFTs if logged in */} + {loggedIn ? ( +
+ Address: {user.addr} +
+ ) : ( +
+ Please log in to view your linked NFTs. +
+ )} + + {/* Login/Logout Button */} + +
+ + {/* Display NFTs if logged in */} + {loggedIn && } +
+
+ ); +} +``` + +Your app will now look like the [simple onchain app] demo! + +## Conclusion + +In this tutorial, you took your first steps towards building powerful new experiences that meet you customers where they are. They can keep their assets in the wallet associate with one app, but also give your app the ability to use them - seamlessly, safely, and beautifully! + +[Account Linking]: ./account-linking/index.md +[NBA Top Shot]: https://nbatopshot.com +[simple onchain app]: https://nextjs-topshot-account-linking.vercel.app +[Dapper Wallet]: https://meetdapper.com +[Cadence]: https://cadence-lang.org/docs +[Next.js]: https://nextjs.org/docs/app/getting-started/installation +[Yarn]: https://yarnpkg.com +[Flow CLI]: ../../tools/flow-cli/index.md +[other installation methods]: ../../tools/flow-cli/install.md +[initializes a project]: ../../tools/flow-cli/super-commands.md#init +[Flow Dependency Manager]: ../../tools/flow-cli/dependency-manager.md +[FCL]: ../../tools/clients/fcl-js/index.md +[App Quickstart Guide]: ./flow-app-quickstart.md +[Wallet Connect]: https://cloud.walletconnect.com/sign-in +[Flow Wallet]: https://wallet.flow.com +[link your Dapper Wallet]: https://support.meetdapper.com/hc/en-us/articles/20744347884819-Account-Linking-and-FAQ \ No newline at end of file diff --git a/docs/build/guides/flow-wallet.png b/docs/build/guides/flow-wallet.png new file mode 100644 index 0000000000..e9e9695c1f Binary files /dev/null and b/docs/build/guides/flow-wallet.png differ diff --git a/docs/build/guides/welcome.png b/docs/build/guides/welcome.png new file mode 100644 index 0000000000..f65093e060 Binary files /dev/null and b/docs/build/guides/welcome.png differ