Browse Source

feat: added Heystack example app

feat/shiki-twoslash-rehype
Patrick Gray 4 years ago
committed by Patrick Gray
parent
commit
92e46355b3
  1. 15
      next.config.js
  2. 1
      public/images/pages/heystack-app.svg
  3. 12
      src/common/navigation.yaml
  4. 0
      src/pages/build-apps/examples/angular.md
  5. 619
      src/pages/build-apps/examples/heystack.md
  6. 0
      src/pages/build-apps/examples/indexing.md
  7. 0
      src/pages/build-apps/examples/public-registry.md
  8. 0
      src/pages/build-apps/examples/todos.md
  9. 4
      src/pages/build-apps/overview.md

15
next.config.js

@ -8,6 +8,21 @@ const withFonts = require('next-fonts');
async function redirects() {
return [
{
source: '/build-apps/tutorials/todos',
destination: '/build-apps/examples/todos',
permanent: true,
},
{
source: '/build-apps/tutorials/public-registry',
destination: '/build-apps/examples/public-registry',
permanent: true,
},
{
source: '/build-apps/tutorials/angular',
destination: '/build-apps/examples/angular',
permanent: true,
},
{
source: '/browser/todo-list.html',
destination: '/build-apps/tutorials/todos',

1
public/images/pages/heystack-app.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 59 KiB

12
src/common/navigation.yaml

@ -55,12 +55,13 @@ sections:
- path: /guides/transaction-signing
- path: /guides/data-storage
- title: Tutorials
- title: Example Apps
usePageTitles: true
pages:
- path: /tutorials/todos
- path: /tutorials/public-registry
- path: /tutorials/angular
- path: /examples/todos
- path: /examples/heystack
- path: /examples/public-registry
- path: /examples/angular
- title: Stacks.js References
usePageTitles: true
@ -77,6 +78,9 @@ sections:
- external:
href: 'https://github.com/blockstack/stacks.js/tree/master/packages/transactions'
title: transactions
- external:
href: 'https://github.com/blockstack/stacks-blockchain-api/tree/master/client'
title: blockchain-api-client
- external:
href: 'https://github.com/blockstack/stacks.js/tree/master/packages/stacking'
title: 'stacking'

0
src/pages/build-apps/tutorials/angular.md → src/pages/build-apps/examples/angular.md

619
src/pages/build-apps/examples/heystack.md

@ -0,0 +1,619 @@
---
title: Heystack app
description: Interacting with the wallet and smart contracts from a React application
tags:
- example-app
images:
large: /images/pages/heystack-app.svg
---
## Introduction
This example application demonstrates important features of the Stacks blockchain, and is a case study for how a frontend
web application can interact with a Clarity smart contract. The full source of the application is provided, this page
highlights important code snippets and design patterns to help you learn how to develop your own Stacks application.
This app highlights the following platform features:
- Authenticating users with the web wallet
- Using a smart contract to store data on the blockchain
- Minting new fungible tokens with a [SIP-010][] compliant smart contract
- Creating and monitoring transactions on the Stacks blockchain using [Stacks.js][]
You can access the [online version][heystack] of the Heystack app to interact with it. The source for Heystack is also
available on [Github][heystack_gh]. This page assumes some familiarity with [React][].
## Heystack overview
Heystack is a web application for chatting with other Stacks users. The application uses the [Stacks web wallet][] to
authenticate users in the frontend. When a user logs in to Heystack, they're given a genesis amount of $HEY fungible
tokens, which allows them to send and like messages on the platform.
Heystack is powered by Clarity smart contracts so each message is a transaction on the Stacks blockchain. Each time a
user sends a message on the platform, they must sign the message with the [Stacks web wallet][] (or another compatible
wallet) and pay a small gas fee in STX. A user spends a $HEY token to send every message, and recieves a $HEY token for
every like that their messages receive.
## Review smart contracts
Heystack depends on two smart contracts to execute the backend functions of the app on the Stacks blockchain: a contract
for handling the messaging content, and a contract for minting and distributing the $HEY token.
### Content contract
The `hey.clar` contract provides two primary functions for the application, one to publish content to
the blockchain and another to like a piece of content based on its ID. This section reviews the implementation of
these primary functions, but is not a comprehensive discussion of the contract.
In order to accomplish the two primary functions, the contract relies on a data variable `content-index` and two
[data maps][], `like-state` and `publisher-state` which contain the number of likes a piece of content has received, and
the principal address of the account that published the content.
Note that all variables are defined at the top of the contract, which is a requirement of the Clarity language. These
include constants such as the `contract-creator`, error codes, and a treasury address.
```clarity
;;
;; Data maps and vars
(define-data-var content-index uint u0)
(define-read-only (get-content-index)
(ok (var-get content-index))
)
(define-map like-state
{ content-index: uint }
{ likes: uint }
)
(define-map publisher-state
{ content-index: uint }
{ publisher: principal }
)
```
Read-only functions provide a method for getting the like count of a piece of content, and getting the principal address
of the message publisher.
```clarity
(define-read-only (get-like-count (id uint))
;; Checks map for like count of given id
;; defaults to 0 likes if no entry found
(ok (default-to { likes: u0 } (map-get? like-state { content-index: id })))
)
(define-read-only (get-message-publisher (id uint))
;; Checks map for like count of given id
;; defaults to 0 likes if no entry found
(ok (unwrap-panic (get publisher (map-get? publisher-state { content-index: id }))))
```
The `get-like-count` method accepts a content ID and returns the number of likes associated with that content. The
method uses the [`default-to`][] function to return `0` if the content ID isn't found in the map of likes.
The `get-message-publisher` method accepts a content ID and returns the principal address of the content publisher. The
method uses the [`unwrap-panic`][] function to halt execution of the method if the principal address isn't found in
the map of publishers.
The two primary public methods are the `send-message` and `like-message` functions. These methods allow the contract
caller to store a message on the blockchain (creating entries in the data maps for the message sender and the number
of likes). Note that the message itself isn't stored in a contract variable, the frontend application reads the content
of the message directly from the transaction on the blockchain.
```clarity
;;
;; Public functions
(define-public (send-message (content (string-utf8 140)))
(let ((id (unwrap! (increment-content-index) (err u0))))
(print { content: content, publisher: tx-sender, index: id })
(map-set like-state
{ content-index: id }
{ likes: u0 }
)
(map-set publisher-state
{ content-index: id }
{ publisher: tx-sender }
)
(transfer-hey u1 HEY_TREASURY)
)
)
```
The `send-message` method accepts a utf-8 string with a maximum length of 140 characters. The method defines an internal
variable `id` using the `let` function and assigns the next content ID to that variable by calling the
`increment-contract-index` method of the contract. The value assignment of this variable is bound by the [`unwrap!`][]
function, which returns an error and exits the control-flow if the `increment-contract-index` function isn't
successfully called.
The method then assigns `u0` likes to the content in the `like-state` data map, and adds the principal address to the
`publisher-state` data map using the [`map-set`][] function. Finally, the private method `transfer-hey` is called to
transfer 1 $HEY token from the message sender to the $HEY treasury address stored in the `HEY_TREASURY` constant.
```clarity
(define-public (like-message (id uint))
(begin
;; cannot like content that doesn't exist
(asserts! (>= (var-get content-index) id) (err ERR_CANNOT_LIKE_NON_EXISTENT_CONTENT))
;; transfer 1 HEY to the principal that created the content
(map-set like-state
{ content-index: id }
{ likes: (+ u1 (get likes (unwrap! (get-like-count id) (err u0)))) }
)
(transfer-hey u1 (unwrap-panic (get-message-publisher id)))
)
)
```
The `like-message` method accepts a content ID. The method checks that the ID is lower than the current content ID using
the [`asserts!`][] function, to verify that the provided ID is a valid ID. If the [`asserts!`][] assessment is `false`,
the method returns an error code. If the ID is valid, the method performs a [`map-set`][] to look up the content in the
`like-state` data map and add a like to the value stored in the map. Once again, the [`unwrap!`][] function is used to
ensure that an invalid value isn't stored in the map.
The `hey.clar` contract provides some additional functions for working with the $HEY token contract, discussed in the
next section.
### Token contract
Heystack creates a native fungible token for use in the application. When a user authenticates with Heystack, they're
automatically eligible to claim 100 $HEY tokens to allow them to start messaging.
[SIP-010][] defines the fungible token standard on Stacks, which allows Stacks compatible wallets to handle fungible
tokens through a set of standardized methods. SIP-010 defines 7 traits that a fungible token contract must have in order
to be compliant:
- `transfer`: method for transferring the token from one principal to another
- `get-name`: returns the human-readable name of the token
- `get-symbol`: returns the ticker symbol of the token
- `get-decimals`: returns number of decimal places in the token
- `get-balance`: return the balance of a given principal
- `get-total-supply`: returns the total supply of the token
- `get-token-uri`: returns an optional string that resolves to a valid URI for the token's metadata.
In Clarity, a contract can declare that it intends to implement a set of standard traits.
```clarity
;; Implement the `ft-trait` trait defined in the `ft-trait` contract
;; https://github.com/hstove/stacks-fungible-token
(impl-trait 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.ft-trait.sip-010-trait)
```
The [`impl-trait`][] function asserts that the smart contract is fully implementing a given set of traits defined by the
argument. Like variable definitions, `impl-trait` must be declared at the top of a smart contract definition.
-> The contract address for SIP-010 trait definition is different depending on which network (mainnet, testnet, etc.)
your contract is deployed on. See the standard for the current addresses of the standard traits.
The `hey-token.clar` contract implements the required 7 traits of [SIP-010][], and one additional method, the
`gift-tokens` method, that allows a principal to request tokens from the contract.
```clarity
(define-public (gift-tokens (recipient principal))
(begin
(asserts! (is-eq tx-sender recipient) (err u0))
(ft-mint? hey-token u1 recipient)
)
)
```
## Authentication
Authentication is handled through the [`@stacks/connect-react`][] and [`@stacks/auth`][] packages, which interact with
compatible Stacks wallet extensions and provide methods for interacting with a user session respectively. [Jotai][]
provides application state management.
The [connect wallet button component][] implements the interface with the Stacks web wallet through the
[`@stacks/connect-react`][] package.
```tsx
import { Button } from '@components/button';
import React from 'react';
import { useConnect } from '@stacks/connect-react';
import { ButtonProps } from '@stacks/ui';
import { useLoading } from '@hooks/use-loading';
import { LOADING_KEYS } from '@store/ui';
export const ConnectWalletButton: React.FC<ButtonProps> = props => {
const { doOpenAuth } = useConnect();
const { isLoading, setIsLoading } = useLoading(LOADING_KEYS.AUTH);
return (
<Button
isLoading={isLoading}
onClick={() => {
void setIsLoading(true);
doOpenAuth();
}}
{...props}
>
Connect wallet
</Button>
);
};
```
Once connected, [`/src/store/auth.ts`][] populates the user session data into the Jotai store, allowing the application
to access the user information.
You can see in the [welcome panel component][] how the presence or absence of stored user data is used to display the
wallet connect button or the signed in view.
```tsx
...
const UserSection = memo((props: StackProps) => {
const { user } = useUser();
return (
<Stack
alignItems="center"
justifyContent="center"
flexGrow={1}
spacing="loose"
textAlign="center"
{...props}
>
{!user ? <SignedOutView /> : <SignedInView onClick={() => console.log('click')} />}
</Stack>
);
});
...
```
### Token faucet
The `use-claim-hey.ts` file provides a React hook for interacting with the token faucet of the Clarity smart contract.
```ts
import { useLoading } from '@hooks/use-loading';
import { LOADING_KEYS } from '@store/ui';
import { useConnect } from '@stacks/connect-react';
import { useNetwork } from '@hooks/use-network';
import { useCallback } from 'react';
import { useHeyContract } from '@hooks/use-hey-contract';
import { REQUEST_FUNCTION } from '@common/constants';
import { principalCV } from '@stacks/transactions/dist/clarity/types/principalCV';
import { useCurrentAddress } from '@hooks/use-current-address';
export function useHandleClaimHey() {
const address = useCurrentAddress();
const { setIsLoading } = useLoading(LOADING_KEYS.CLAIM_HEY);
const { doContractCall } = useConnect();
const [contractAddress, contractName] = useHeyContract();
const network = useNetwork();
const onFinish = useCallback(() => {
void setIsLoading(false);
}, [setIsLoading]);
const onCancel = useCallback(() => {
void setIsLoading(false);
}, [setIsLoading]);
return useCallback(() => {
void setIsLoading(true);
void doContractCall({
contractAddress,
contractName,
functionName: REQUEST_FUNCTION,
functionArgs: [principalCV(address)],
onFinish,
onCancel,
network,
stxAddress: address,
});
}, [setIsLoading, onFinish, network, onCancel, address, doContractCall]);
}
```
The [`@stacks/connect-react`][] package exports the `doContractCall` method, which interacts with the smart contract on
the blockchain. There are more examples of transaction calls in the next section. It's important to note that it's
necessary to convert Javascript types to Clarity types using the types exported by the [`@stacks/transactions`][] package.
Further discussion of this conversion is in the [Clarity types in Javascript][] section.
## Transactions
Since messages in Heystack are transactions against a Clarity smart contract, the application must be able to create
transactions and read their content from the blockchain. The following sections highlight code snippets that perform
Clarity transactions and read both completed and pending transactions from the Stacks blockchain.
### Issuing transactions
The two primary functions of the `hey.clar` smart contract are publishing a message and accepting a like on an already
published message. The [`src/hooks/use-publish-hey.ts`][] file implements the frontend method for calling the smart
contract on the blockchain with the appropriate values.
```ts
...
return useCallback(
(content: string, _onFinish: () => void) => {
void setShowPendingOverlay(true);
void setIsLoading(true);
void doContractCall({
contractAddress,
contractName,
functionName: MESSAGE_FUNCTION,
functionArgs: [
stringUtf8CV(content),
attachmentUri !== '' ? someCV(stringUtf8CV(attachmentUri)) : noneCV(),
],
onFinish: () => {
_onFinish();
onFinish();
},
postConditions: [
createFungiblePostCondition(
address,
FungibleConditionCode.Equal,
new BN(1),
createAssetInfo(contractAddress, 'hey-token', 'hey-token')
),
],
onCancel,
network,
stxAddress: address,
});
},
[setIsLoading, onFinish, network, onCancel, address, doContractCall]
);
...
```
The frontend uses the `doContractCall` function from the [`@stacks/connect-react`][] package to perform the call to the
Clarity smart contract. In order to support the mapping of [Javascript types to Clarity types][], helpers exported from
the [`@stacks/transactions`][] package are used as arguments to the contract call.
Note that the contract call also create post conditions to verify that a single $HEY token is transferred by the
execution of the contract call. Post conditions are a powerful feature of Clarity that can be used to prevent
rug-pulling and other detrimental behavior by smart contracts.
### Reading transactions
Heystack achieves pseudo-real-time messaging by reading both confirmed and pending transactions from the blockchain.
Pending transactions are read from the mempool, whereas confirmed transactions are read directly from the chain. The
[`src/store/hey.ts`][] file contains the implementation of both.
```ts
...
export const heyTransactionsAtom = atomWithQuery<ContractCallTransaction[], string>(get => ({
queryKey: ['hey-txs'],
...(defaultOptions as any),
refetchInterval: 500,
queryFn: async (): Promise<ContractCallTransaction[]> => {
const client = get(accountsClientAtom);
const txClient = get(transactionsClientAtom);
const txs = await client.getAccountTransactions({
limit: 50,
principal: HEY_CONTRACT,
});
const txids = (txs as TransactionResults).results
.filter(
tx =>
tx.tx_type === 'contract_call' &&
tx.contract_call.function_name === MESSAGE_FUNCTION &&
tx.tx_status === 'success'
)
.map(tx => tx.tx_id);
const final = await Promise.all(txids.map(async txId => txClient.getTransactionById({ txId })));
return final as ContractCallTransaction[];
},
}));
...
```
The `getAccountTransactions` from the `AccountsApi` object exported by [`@stacks/blockchain-api-client`][] is used to
read confirmed blockchain transactions against the `hey.clar` contract from the Stacks API. The list of transactions
returned by the API is filtered to only transactions representing a call to the message function that was successful,
and then mapped to an array of transaction IDs.
Finally, the array of IDs is used to read each full transaction from the blockchain using the `getTransactionsById`
method from the `TransactionsApi` object exported by the [`@stacks/blockchain-api-client`][] package.
Pending transactions are read from the mempool in a similar implementation.
```ts
export const pendingTxsAtom = atomWithQuery<Heystack[], string>(get => ({
queryKey: ['hey-pending-txs'],
refetchInterval: 1000,
...(defaultOptions as any),
queryFn: async (): Promise<Heystack[]> => {
const client = get(transactionsClientAtom);
const txs = await client.getMempoolTransactionList({ limit: 96 });
const heyTxs = (txs as MempoolTransactionListResponse).results
.filter(
tx =>
tx.tx_type === 'contract_call' &&
tx.contract_call.contract_id === HEY_CONTRACT &&
tx.contract_call.function_name === MESSAGE_FUNCTION &&
tx.tx_status === 'pending'
)
.map(tx => tx.tx_id);
const final = await Promise.all(heyTxs.map(async txId => client.getTransactionById({ txId })));
return (
(final as ContractCallTransaction[]).map(tx => {
const attachment = tx.contract_call.function_args?.[1].repr
.replace(`(some u"`, '')
.slice(0, -1);
return {
sender: tx.sender_address,
content: tx.contract_call.function_args?.[0].repr
.replace(`u"`, '')
.slice(0, -1) as string,
id: tx.tx_id,
attachment: attachment === 'non' ? undefined : attachment,
timestamp: (tx as any).receipt_time,
isPending: true,
};
}) || []
);
},
}));
```
Pending transactions are read from the mempool using the `getMempoolTransactionList` method from the `TransactionsApi`
exported by [`@stacks/blockchain-api-client`][]. Similar to confirmed transactions, the returned array is filtered to
a list of IDs, and then used to generate an array of full transactions.
Because of differences in the data structure of the pending transactions vs. confirmed transactions, the pending
transaction list must be standardized before being returned.
Note that for the low stakes of a messaging app, pending transactions can be treated as likely permanent state
transitions. For applications implementing higher stakes business logic (such as the transfer of representations
of value) it would be more appropriate to wait to display confirmed transactions.
### Clarity types in Javascript
In order to create transactions to call functions in Clarity contracts, the [`@stacks/transactions`][] package exports
classes that make it easy to construct well-typed Clarity values in Javascript. According to the Clarity language
specification, Clarity has the following types:
- `(tuple (key-name-0 key-type 0) (key-name-1 key-type-1) ...)` - a typed tuple with named fields
- `(list max-len entry-type)` - a list of maximum length `max-len`, with entries of type `entry-type`
- `(response ok-type err-type)` - object used by public functions to commit their state changes or abort
- `(optional some-type)` - an option type for objects that can be either `(some-value)` or `none`
- `(buff max-len)` - byte buffer of maximum length
- `principal` - object representing a principal address (contract or standard)
- `bool` - boolean value (`true` or `false`)
- `int` - signed 128-bit integer
- `uint` - unsigned 128-bit integer
To support these types in Javascript, [`@stacks/transactions`][] exports the following helpers:
```ts
// construct boolean clarity values
const t = trueCV();
const f = falseCV();
// construct optional clarity values
const nothing = noneCV();
const something = someCV(t);
// construct a buffer clarity value from an existing Buffer
const buffer = Buffer.from('foo');
const bufCV = bufferCV(buffer);
// construct signed and unsigned integer clarity values
const i = intCV(-10);
const u = uintCV(10);
// construct principal clarity values
const address = 'SP2JXKMSH007NPYAQHKJPQMAQYAD90NQGTVJVQ02B';
const contractName = 'contract-name';
const spCV = standardPrincipalCV(address);
const cpCV = contractPrincipalCV(address, contractName);
// construct response clarity values
const errCV = responseErrorCV(trueCV());
const okCV = responseOkCV(falseCV());
// construct tuple clarity values
const tupCV = tupleCV({
a: intCV(1),
b: trueCV(),
c: falseCV(),
});
// construct list clarity values
const l = listCV([trueCV(), falseCV()]);
```
You should use these helpers when calling Clarity contracts with Javascript to avoid failed contract calls due to bad
typing.
## Reading BNS names
An important feature of Stacks is the [Blockchain Naming System][] (BNS). BNS allows users to register a human-readable
identity to their account, that can act as both a username and a web address.
Names registered to a user can be read from a Stacks API endpoint, as demonstrated in [`src/store/names.ts`][].
-> Due to ecosystem limitations, it's currently uncommon for BNS names to be registered on any testnet. For the purpose
of demonstration, Heystack looks for BNS names against the user's mainnet wallet address.
```ts
export const namesAtom = atomFamily((address: string) =>
atom(async get => {
if (!address || address === '') return;
const network = get(mainnetNetworkAtom);
if (!network) return null;
const local = getLocalNames(network.coreApiUrl, address);
if (local) {
const [names, timestamp] = local;
const now = Date.now();
const isStale = now - timestamp > STALE_TIME;
if (!isStale) return names;
}
try {
const names = await fetchNamesByAddress({
networkUrl: network.coreApiUrl,
address,
});
if (names?.length) {
setLocalNames(network.coreApiUrl, address, [names, Date.now()]);
}
return names || [];
} catch (e) {
console.error(e);
return [];
}
})
);
```
In order to reduce network traffic, Heystack also caches names in the browser's local storage.
A common design pattern in Stacks 2.0 apps is to check if a user has a registered BNS name (only 1 name can be tied to
an account) and display that name in the app where appropriate. If the user doesn't own a BNS name, the wallet address
is used as a stand in. Often, the wallet address is truncated to avoid displaying an overly long string.
The account name component in [`src/components/user-area.tsx`][] demonstrates this design pattern:
```tsx
...
const AccountNameComponent = memo(() => {
const { user } = useUser();
const address = useCurrentMainnetAddress();
const names = useAccountNames(address);
const name = names?.[0];
return <Text mb="tight">{name || user?.username || truncateMiddle(address)}</Text>;
});
...
```
[heystack]: https://heystack.xyz
[stacks.js]: https://github.com/blockstack/stacks.js
[stacks web wallet]: https://www.hiro.so/wallet/install-web
[react]: https://reactjs.org/
[heystack_gh]: https://github.com/blockstack/heystack
[data maps]: /references/language-functions#define-map
[`default-to`]: /references/language-functions#default-to
[`asserts!`]: /references/language-functions#asserts
[`unwrap-panic`]: /references/language-functions#unwrap-panic
[`unwrap!`]: /references/language-functions#unwrap
[`map-set`]: /references/language-functions#map-set
[sip-010]: https://github.com/hstove/sips/blob/feat/sip-10-ft/sips/sip-010/sip-010-fungible-token-standard.md
[`impl-trait`]: /references/language-functions#impl-trait
[`@stacks/connect-react`]: https://github.com/blockstack/connect#readme
[`@stacks/auth`]: https://github.com/blockstack/stacks.js/tree/master/packages/auth
[jotai]: https://github.com/pmndrs/jotai
[connect wallet button component]: https://github.com/blockstack/heystack/blob/main/src/components/connect-wallet-button.tsx
[welcome panel component]: https://github.com/blockstack/heystack/blob/63ce30f4f6de7a9c846fcdba3acbb6c7b82b83e3/src/components/welcome-panel.tsx#L102
[`/src/store/auth.ts`]: https://github.com/blockstack/heystack/blob/main/src/store/auth.ts
[clarity types in javascript]: /build-apps/examples/heystack#clarity-types-in-javascript
[`@stacks/transactions`]: https://github.com/blockstack/stacks.js/tree/master/packages/transactions#constructing-clarity-values
[blockchain naming system]: /build-apps/references/bns
[`src/store/names.ts`]: https://github.com/blockstack/heystack/blob/main/src/store/names.ts
[javascript types to clarity types]: /build-apps/examples/heystack#clarity-types-in-javascript
[`@stacks/blockchain-api-client`]: https://github.com/blockstack/stacks-blockchain-api/tree/master/client
[`src/common/hooks/use-publish-hey.ts`]: https://github.com/blockstack/heystack/blob/main/src/common/hooks/use-publish-hey.ts
[`src/store/hey.ts`]: https://github.com/blockstack/heystack/blob/main/src/store/hey.ts
[`src/components/user-area.tsx`]: https://github.com/blockstack/heystack/blob/22e4e9020f8bbb404e8c1e36f32f000050f90818/src/components/user-area.tsx#L62

0
src/pages/build-apps/tutorials/indexing.md → src/pages/build-apps/examples/indexing.md

0
src/pages/build-apps/tutorials/public-registry.md → src/pages/build-apps/examples/public-registry.md

0
src/pages/build-apps/tutorials/todos.md → src/pages/build-apps/examples/todos.md

4
src/pages/build-apps/overview.md

@ -29,7 +29,7 @@ While integration is possible for any type of app, most of the resources availab
[@page-reference | grid]
| /build-apps/guides/authentication, /build-apps/guides/transaction-signing, /build-apps/guides/data-storage
## Tutorials
## Example apps
[@page-reference | grid]
| /build-apps/tutorials/todos, /build-apps/tutorials/public-registry, /build-apps/tutorials/angular, /build-apps/tutorials/radiks
| /build-apps/examples/todos, /build-apps/examples/heystack, /build-apps/examples/public-registry, /build-apps/examples/angular

Loading…
Cancel
Save