Skip to content

Commit

Permalink
fix: update frontend tutorial to latest libs
Browse files Browse the repository at this point in the history
And just make sure it works, in general. It was pretty broken.

Relies on these fixes:

- stellar/soroban-template-astro#13
- stellar/soroban-template-astro#14

And can be cleaned up once this is merged:

- Creit-Tech/Stellar-Wallets-Kit#49
  • Loading branch information
chadoh committed Dec 11, 2024
1 parent a394359 commit 2610e44
Showing 1 changed file with 107 additions and 59 deletions.
166 changes: 107 additions & 59 deletions docs/build/apps/dapp-frontend.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -228,52 +228,87 @@ import {
allowAllModules,
FREIGHTER_ID,
StellarWalletsKit,
WalletNetwork,
} from "@creit.tech/stellar-wallets-kit";

const kit: StellarWalletsKit = new StellarWalletsKit({
const SELECTED_WALLET_ID = "selectedWalletId";

function getSelectedWalletId() {
return localStorage.getItem(SELECTED_WALLET_ID);
}

// TODO: stop exporting all of `kit` pending merge & release of:
// https://github.com/Creit-Tech/Stellar-Wallets-Kit/pull/49
export const kit: StellarWalletsKit = new StellarWalletsKit({
modules: allowAllModules(),
network: WalletNetwork.TESTNET,
selectedWalletId: FREIGHTER_ID,
network: import.meta.env.PUBLIC_STELLAR_NETWORK_PASSPHRASE,
// StellarWalletsKit forces you to specify a wallet, even if the user didn't
// select one yet, so we default to Freighter.
// We'll work around this later in `getPublicKey`.
selectedWalletId: getSelectedWalletId() ?? FREIGHTER_ID,
});

const connectionState: { publicKey: string | undefined } = {
publicKey: undefined,
};
export async function getPublicKey() {
if (!getSelectedWalletId()) return null;
const { address } = await kit.getAddress();
return address;
}

function loadedPublicKey(): string | undefined {
return connectionState.publicKey;
export async function setWallet(walletId: string) {
localStorage.setItem(SELECTED_WALLET_ID, walletId);
kit.setWallet(walletId);
}

function setPublicKey(data: string): void {
connectionState.publicKey = data;
export async function disconnect(callback?: () => Promise<void>) {
localStorage.removeItem(SELECTED_WALLET_ID);
kit.disconnect();
if (callback) await callback();
}

export { kit, loadedPublicKey, setPublicKey };
export async function connect(callback?: () => Promise<void>) {
await kit.openModal({
onWalletSelected: async (option) => {
try {
await setWallet(option.id);
if (callback) await callback();
} catch (e) {
console.error(e);
}
return option.id;
},
});
}
```

In the code above, we created an instance of the kit and two simple functions that will take care of "setting" and "loading" the public key of the user. This lets us use the user's public key elsewhere in our code. The kit is started with Freighter as the default wallet, and the Testnet network as the default network. You can learn more about how the kit works in [the StellarWalletsKit documentation](https://stellarwalletskit.dev/)
In the code above, we instantiate the kit with desired settings and export it. We also wrap some kit functions and add custom functionality, such as augmenting the kit by allowing it to remember which wallet options was selected between page refreshes (that's the `localStorage` bit). The kit requires a `selectedWalletId` even before the user selects one, so we also work around this limitation, as the code comment explains. You can learn more about how the kit works in [the StellarWalletsKit documentation](https://stellarwalletskit.dev/)

Now we're going to add a "Connect" button to the page which will open the kit's built in modal, and prompt the user to use their preferred wallet. Once the user picks their preferred wallet and grants permission to accept requests from the website, we will fetch the public key and the "Connect" button will be replaced with a message saying, "Signed in as [their public key]".
Now we're going to add a "Connect" button to the page which will open the kit's built-in modal, and prompt the user to use their preferred wallet. Once the user picks their preferred wallet and grants permission to accept requests from the website, we will fetch the public key and the "Connect" button will be replaced with a message saying, "Signed in as [their public key]".

Now let's add a new component to the `src/components` directory called `ConnectWallet.astro` with the following content:

```html title="src/components/ConnectWallet.astro"
<div id="connect-wrap" class="wrap" aria-live="polite">
<div class="ellipsis">
<button data-connect aria-controls="connect-wrap">Connect</button>
</div>
&nbsp;
<div class="ellipsis"></div>
<button style="display:none" data-connect aria-controls="connect-wrap">
Connect
</button>
<button style="display:none" data-disconnect aria-controls="connect-wrap">
Disconnect
</button>
</div>

<style>
.wrap {
text-align: center;
display: flex;
width: 18em;
margin: auto;
justify-content: center;
line-height: 2.7rem;
gap: 0.5rem;
}
.ellipsis {
line-height: 2.7rem;
margin: auto;
max-width: 12rem;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
Expand All @@ -282,38 +317,48 @@ Now let's add a new component to the `src/components` directory called `ConnectW
</style>

<script>
import { kit, setPublicKey } from "../stellar-wallets-kit";
const ellipsis = document.querySelector("#connect-wrap .ellipsis");
const button = document.querySelector("[data-connect]");
async function setLoggedIn(publicKey: string) {
ellipsis.innerHTML = `Signed in as ${publicKey}`;
ellipsis.title = publicKey;
import { getPublicKey, connect, disconnect } from "../stellar-wallets-kit";
const ellipsis = document.querySelector(
"#connect-wrap .ellipsis",
) as HTMLElement;
const connectButton = document.querySelector("[data-connect]") as HTMLButtonElement;
const disconnectButton = document.querySelector(
"[data-disconnect]",
) as HTMLButtonElement;
async function showDisconnected() {
ellipsis.innerHTML = "";
ellipsis.removeAttribute("title");
connectButton.style.removeProperty("display");
disconnectButton.style.display = "none";
}
button.addEventListener("click", async () => {
button.disabled = true;
try {
await kit.openModal({
onWalletSelected: async (option) => {
try {
kit.setWallet(option.id);
const { address } = await kit.getAddress();
setPublicKey(address);
await setLoggedIn(address);
} catch (e) {
console.error(e);
}
},
});
} catch (e) {
console.error(e);
async function showConnected() {
const publicKey = await getPublicKey();
if (publicKey) {
ellipsis.innerHTML = `Signed in as ${publicKey}`;
ellipsis.title = publicKey ?? "";
connectButton.style.display = "none";
disconnectButton.style.removeProperty("display");
} else {
showDisconnected();
}
}
connectButton.addEventListener("click", async () => {
await connect(showConnected);
});
button.disabled = false;
disconnectButton.addEventListener("click", async () => {
disconnect(showDisconnected);
});
if (await getPublicKey()) {
showConnected();
} else {
showDisconnected();
}
</script>
```
Expand All @@ -325,7 +370,7 @@ And all the `script` declarations get bundled together and included intelligentl
You can read more about this in [Astro's page about client-side scripts](https://docs.astro.build/en/guides/client-side-scripts/).
The code itself here is pretty self-explanatory. We import the wallets kit from the file we created before. Then, when the user clicks on the button, we launch the built-in modal do display to the user connection options. Once the user picks their preferred wallet, we set it as the wallets kit's default wallet before requesting and saving the address.
The code itself here is pretty self-explanatory. We import `kit` from the file we created before. Then, when the user clicks on the sign-in button, we call the `connect` function we created in our `stellar-wallets-kit.ts` file above. This will launch the built-in StellarWalletsKit modal, which allows the user to pick from the wallet options we configured (we configured all of them, with `allowAllModules`). We pass our own `setLoggedIn` function as the callback, which will be called in the `onWalletSelected` function in `stellar-wallets-kit.ts`. We end by updating the UI, based on whether the user is currently connected or not.
Now we can import the component in the frontmatter of `pages/index.astro`:
Expand All @@ -341,7 +386,7 @@ Now we can import the component in the frontmatter of `pages/index.astro`:
And add it right below the `<h1>`:
```diff title="pages/index.astro"
<h1>{greeting}</h1>
<h1>{greeting}</h1>
+<ConnectWallet />
```
Expand All @@ -366,13 +411,15 @@ Current value: <strong id="current-value" aria-live="polite">???</strong><br />
<button data-increment aria-controls="current-value">Increment</button>
<script>
import { kit, loadedPublicKey } from "../stellar-wallets-kit";
import { getPublicKey, kit } from "../stellar-wallets-kit";
import incrementor from "../contracts/soroban_increment_contract";
const button = document.querySelector("[data-increment]");
const currentValue = document.querySelector("#current-value");
const button = document.querySelector(
"[data-increment]",
) as HTMLButtonElement;
const currentValue = document.querySelector("#current-value") as HTMLElement;
button.addEventListener("click", async () => {
const publicKey = loadedPublicKey();
const publicKey = await getPublicKey();
if (!publicKey) {
alert("Please connect your wallet first");
Expand All @@ -391,9 +438,10 @@ Current value: <strong id="current-value" aria-live="polite">???</strong><br />
try {
const { result } = await tx.signAndSend({
// this silliness can be cleaned up pending merge of
// https://github.com/Creit-Tech/Stellar-Wallets-Kit/pull/49
signTransaction: async (xdr) => {
const { signedTxXdr } = await kit.signTransaction(xdr);
return signedTxXdr;
return await kit.signTransaction(xdr);
},
});
Expand All @@ -402,17 +450,17 @@ Current value: <strong id="current-value" aria-live="polite">???</strong><br />
currentValue.innerHTML = result.toString();
} catch (e) {
console.error(e);
} finally {
button.disabled = false;
button.classList.remove("loading");
}
button.disabled = false;
button.classList.remove("loading");
});
</script>
```
This should be somewhat familiar by now. We have a `script` that, thanks to Astro's build system, can `import` modules directly. We use `document.querySelector` to find the elements defined above. And we add a `click` handler to the button, which calls `increment` and updates the value on the page. It also sets the button to `disabled` and adds a `loading` class while the call is in progress to prevent the user from clicking it again and visually communicate that something is happening. For people using screen readers, the loading state is communicated with the [visually-hidden](https://www.a11yproject.com/posts/how-to-hide-content/) span, which will be announced to them thanks to the `aria` tags we saw before.
The biggest difference from the call to `greeter.hello` is that this transaction gets executed in two steps. The initial call to `increment` constructs a Soroban transaction and then makes an RPC call to _simulate_ it. For read-only calls like `hello`, this is all you need, so you can get the `result` right away. For write calls like `increment`, you then need to `signAndSend` before the transaction actually gets included in the ledger.
The biggest difference from the call to `greeter.hello` is that this transaction gets executed in two steps. The initial call to `increment` constructs a Soroban transaction and then makes an RPC call to _simulate_ it. For read-only calls like `hello`, this is all you need, so you can get the `result` right away. For write calls like `increment`, you then need to `signAndSend` before the transaction actually gets included in the ledger. You also need to make sure you set a valid `publicKey` and a `signTransaction` method.
:::info
Expand Down

0 comments on commit 2610e44

Please sign in to comment.