Skip to content

Commit 5b9559a

Browse files
Add localtunnel, auto-network switch (#6)
* added localtunnel * send on mainnet * upd up-provider, add search * network switch, readme update * dependency updated & address build warnings * fix: Repair build and yarn * address build warnings, update up-provider client, ui button fix --------- Co-authored-by: Andreas Richter <[email protected]>
1 parent 70dbcc6 commit 5b9559a

File tree

13 files changed

+6949
-10406
lines changed

13 files changed

+6949
-10406
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,30 +37,30 @@ jobs:
3737
- uses: actions/setup-node@v3 # Latest version as of now
3838
with:
3939
node-version: '22.12.0'
40-
cache: 'npm'
40+
cache: 'yarn'
4141

4242
- name: ⚙️ Install dependencies
43-
run: npm install
43+
run: yarn install
4444

4545
- name: Build
4646
run: |
47-
npx @cloudflare/next-on-pages
47+
yarn dlx @cloudflare/next-on-pages
4848
4949
- name: 'Deploy release'
5050
if: ${{ steps.extract_branch.outputs.branch_name == '' }}
5151
env:
5252
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
5353
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
5454
run: |
55-
npx wrangler pages deploy --project-name "${{ secrets.CF_PROJECT_NAME }}" .vercel/output/static
55+
yarn dlx wrangler pages deploy --project-name "${{ secrets.CF_PROJECT_NAME }}" .vercel/output/static
5656
5757
- name: Deploy ${{ steps.extract_branch.outputs.branch_name }} (PR)
5858
if: ${{ steps.extract_branch.outputs.branch_name != '' }}
5959
env:
6060
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
6161
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
6262
run: |
63-
npx wrangler pages deploy --project-name "${{ secrets.CF_PROJECT_NAME }}" --branch "${{ steps.extract_branch.outputs.branch_name }}" .vercel/output/static | tee output.log
63+
yarn dlx wrangler pages deploy --project-name "${{ secrets.CF_PROJECT_NAME }}" --branch "${{ steps.extract_branch.outputs.branch_name }}" .vercel/output/static | tee output.log
6464
sed < output.log -n 's#.*Take a peek over at \(.*\)$#specific_url=\1#p' >> $GITHUB_OUTPUT
6565
id: deploy
6666

.yarn/releases/yarn-4.6.0.cjs

Lines changed: 934 additions & 0 deletions
Large diffs are not rendered by default.

.yarnrc.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
nodeLinker: node-modules
2+
3+
yarnPath: .yarn/releases/yarn-4.6.0.cjs

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ This template showcases:
1010
- Integrates the [@lukso/web-components](https://www.npmjs.com/package/@lukso/web-components) library for ready-to-use branded components
1111
- Uses the [erc725js](https://docs.lukso.tech/tools/dapps/erc725js/getting-started) library to fetch profile data from the blockchain
1212

13+
> **Cursor Tip:** You can rename this README.md file to `repo.cursorrules` for better AI development experience using Cursor.
14+
1315
## Key Features
1416

1517
### UP-Provider Integration
@@ -44,6 +46,26 @@ yarn dev
4446

4547
3. Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.(Note that the Grid context is not available in the local environment)
4648

49+
4. Testing your mini-app on the Grid:
50+
51+
We're using `localtunnel` to test the mini-app on the Grid. This library helps us to generate a public URL that can be used to add the mini-app to the Grid.
52+
53+
> Alternatively, you can use free cloud deployment services like Vercel, Replit, etc.
54+
55+
Globally install `localtunnel`:
56+
57+
```bash
58+
npm install -g localtunnel
59+
```
60+
61+
In the second terminal, run:
62+
63+
```bash
64+
lt --port <LOCALHOST_PORT>
65+
```
66+
67+
You can use this URL to add the mini-app to the Grid.
68+
4769
## Project Structure
4870

4971
- `components/upProvider.tsx`: Core UP Provider implementation and wallet connection logic

app/page.tsx

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,48 @@
1-
'use client';
1+
"use client";
22

3-
import { UpProvider } from '@/components/upProvider';
4-
import { Donate } from '@/components/Donate';
5-
import { ProfileSearch } from '@/components/ProfileSearch';
6-
import { useUpProvider } from '@/components/upProvider';
3+
import { UpProvider } from "@/components/upProvider";
4+
import { Donate } from "@/components/Donate";
5+
import { ProfileSearch } from "@/components/ProfileSearch";
6+
import { useUpProvider } from "@/components/upProvider";
7+
import { useState, useEffect } from "react";
78

89
// Import the LUKSO web-components library
9-
import('@lukso/web-components');
10+
let promise: Promise<unknown> | null = null;
11+
if (typeof window !== "undefined") {
12+
promise = import("@lukso/web-components");
13+
}
1014

1115
/**
1216
* Main content component that handles the conditional rendering of Donate and ProfileSearch components.
1317
* Utilizes the UpProvider context to manage selected addresses and search state.
14-
*
18+
*
1519
* @component
1620
* @returns {JSX.Element} A component that toggles between Donate and ProfileSearch views
1721
* based on the isSearching state from UpProvider.
1822
*/
1923
function MainContent() {
24+
const [mounted, setMounted] = useState(false);
25+
26+
useEffect(() => {
27+
// Load web component here if needed
28+
promise?.then(() => {
29+
setMounted(true);
30+
});
31+
}, []);
32+
2033
const { selectedAddress, setSelectedAddress, isSearching } = useUpProvider();
34+
35+
if (!mounted) {
36+
return null; // or a loading placeholder
37+
}
38+
2139
return (
2240
<>
23-
<div className={`${isSearching ? 'hidden' : 'block'}`}>
41+
<div className={`${isSearching ? "hidden" : "block"}`}>
2442
<Donate selectedAddress={selectedAddress} />
2543
</div>
2644

27-
<div className={`${!isSearching ? 'hidden' : 'block'}`}>
45+
<div className={`${!isSearching ? "hidden" : "block"}`}>
2846
<ProfileSearch onSelectAddress={setSelectedAddress} />
2947
</div>
3048
</>
@@ -34,7 +52,7 @@ function MainContent() {
3452
/**
3553
* Root component of the application that wraps the main content with the UpProvider context.
3654
* Serves as the entry point for the donation and profile search functionality.
37-
*
55+
*
3856
* @component
3957
* @returns {JSX.Element} The wrapped MainContent component with UpProvider context
4058
*/

components/Donate.tsx

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export function Donate({ selectedAddress }: DonateProps) {
3636
const [amount, setAmount] = useState<number>(minAmount);
3737
const [error, setError] = useState('');
3838
const recipientAddress = selectedAddress || contextAccounts[0];
39+
const [isLoading, setIsLoading] = useState(false);
3940

4041
const validateAmount = useCallback((value: number) => {
4142
if (value < minAmount) {
@@ -53,12 +54,28 @@ export function Donate({ selectedAddress }: DonateProps) {
5354
}, [amount, validateAmount]);
5455

5556
const sendToken = async () => {
56-
if (!client || !walletConnected || !amount) return;
57-
await client.sendTransaction({
58-
account: accounts[0] as `0x${string}`,
59-
to: recipientAddress as `0x${string}`,
60-
value: parseUnits(amount.toString(), 18),
61-
});
57+
if (!client || !walletConnected || !amount) {
58+
return;
59+
}
60+
61+
try {
62+
setIsLoading(true);
63+
const tx = await client.sendTransaction({
64+
account: accounts[0] as `0x${string}`,
65+
to: recipientAddress as `0x${string}`,
66+
value: parseUnits(amount.toString(), 18),
67+
});
68+
69+
// Wait for transaction confirmation
70+
await client.waitForTransactionReceipt({ hash: tx });
71+
72+
// Reset amount after successful transaction
73+
setAmount(minAmount);
74+
} catch (err) {
75+
console.error('Transaction failed:', err);
76+
} finally {
77+
setIsLoading(false);
78+
}
6279
};
6380

6481
const handleOnInput = useCallback(
@@ -99,6 +116,7 @@ export function Donate({ selectedAddress }: DonateProps) {
99116
variant="primary"
100117
size="medium"
101118
className="mt-2"
119+
isLoading={isLoading}
102120
disabled={!walletConnected}
103121
>
104122
{`Donate ${amount} LYX`}

components/LuksoProfile.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,15 @@ import { useUpProvider } from './upProvider';
2222

2323
// Constants for the IPFS gateway and RPC endpoint for the LUKSO testnet
2424
const IPFS_GATEWAY = 'https://api.universalprofile.cloud/ipfs/';
25-
const RPC_ENDPOINT = 'https://rpc.testnet.lukso.network';
25+
const RPC_ENDPOINT_TESTNET = 'https://rpc.testnet.lukso.network';
26+
const RPC_ENDPOINT_MAINNET = 'https://rpc.mainnet.lukso.network';
2627

2728
interface LuksoProfileProps {
2829
address: string;
2930
}
3031

3132
export function LuksoProfile({ address }: LuksoProfileProps) {
32-
const { setIsSearching } = useUpProvider();
33+
const { setIsSearching, chainId } = useUpProvider();
3334
const [profileData, setProfileData] = useState<{
3435
imgUrl: string;
3536
fullName: string;
@@ -40,7 +41,7 @@ export function LuksoProfile({ address }: LuksoProfileProps) {
4041
imgUrl: 'https://tools-web-components.pages.dev/images/sample-avatar.jpg',
4142
fullName: 'username',
4243
background: 'https://tools-web-components.pages.dev/images/sample-background.jpg',
43-
profileAddress: '0x12345',
44+
profileAddress: '0x1234567890111213141516171819202122232425',
4445
isLoading: false,
4546
});
4647

@@ -52,7 +53,8 @@ export function LuksoProfile({ address }: LuksoProfileProps) {
5253

5354
try {
5455
const config = { ipfsGateway: IPFS_GATEWAY };
55-
const profile = new ERC725(erc725schema, address, RPC_ENDPOINT, config);
56+
const rpcEndpoint = chainId === 42 ? RPC_ENDPOINT_MAINNET : RPC_ENDPOINT_TESTNET;
57+
const profile = new ERC725(erc725schema, address, rpcEndpoint, config);
5658
const fetchedData = await profile.fetchData('LSP3Profile');
5759

5860
if (
@@ -86,7 +88,7 @@ export function LuksoProfile({ address }: LuksoProfileProps) {
8688
}
8789

8890
fetchProfileImage();
89-
}, [address]);
91+
}, [address, chainId]);
9092

9193
return (
9294
<lukso-card
@@ -115,7 +117,7 @@ export function LuksoProfile({ address }: LuksoProfileProps) {
115117
size="small"
116118
isIcon={true}
117119
>
118-
<lukso-icon name="profile-recovery" size="small" color="neutral-20"></lukso-icon>
120+
<lukso-icon name="profile-recovery" size="small" color="neutral-20" class="pl-3 pr-3"></lukso-icon>
119121
</lukso-button>
120122
</lukso-tooltip>
121123
</div>

components/ProfileSearch.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ import { useCallback, useState } from 'react';
2020
import { request, gql } from 'graphql-request';
2121
import makeBlockie from 'ethereum-blockies-base64';
2222
import { useUpProvider } from './upProvider';
23+
import Image from 'next/image';
24+
25+
const ENVIO_TESTNET_URL = 'https://envio.lukso-testnet.universal.tech/v1/graphql';
26+
const ENVIO_MAINNET_URL = 'https://envio.lukso-mainnet.universal.tech/v1/graphql';
2327

2428
const gqlQuery = gql`
2529
query MyQuery($id: String!) {
@@ -57,12 +61,12 @@ type SearchProps = {
5761
};
5862

5963
export function ProfileSearch({ onSelectAddress }: SearchProps) {
60-
const { setIsSearching } = useUpProvider();
64+
const { chainId, setIsSearching } = useUpProvider();
6165
const [query, setQuery] = useState('');
6266
const [results, setResults] = useState<Profile[]>([]);
6367
const [loading, setLoading] = useState(false);
6468
const [showDropdown, setShowDropdown] = useState(false);
65-
69+
6670
const handleSearch = useCallback(
6771
async (searchQuery: string, forceSearch: boolean = false) => {
6872
setQuery(searchQuery);
@@ -80,12 +84,12 @@ export function ProfileSearch({ onSelectAddress }: SearchProps) {
8084

8185
setLoading(true);
8286
try {
87+
const envioUrl = chainId === 42 ? ENVIO_MAINNET_URL : ENVIO_TESTNET_URL;
8388
const { search_profiles: data } = (await request(
84-
'https://envio.lukso-testnet.universal.tech/v1/graphql',
89+
envioUrl,
8590
gqlQuery,
8691
{ id: searchQuery }
8792
)) as { search_profiles: Profile[] };
88-
8993
setResults(data);
9094
setShowDropdown(true);
9195
} catch (error) {
@@ -95,7 +99,7 @@ export function ProfileSearch({ onSelectAddress }: SearchProps) {
9599
setLoading(false);
96100
}
97101
},
98-
[]
102+
[chainId]
99103
);
100104

101105
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
@@ -120,10 +124,12 @@ export function ProfileSearch({ onSelectAddress }: SearchProps) {
120124
const getProfileImage = (profile: Profile) => {
121125
if (profile.profileImages && profile.profileImages.length > 0) {
122126
return (
123-
<img
127+
<Image
124128
src={profile.profileImages[0].src}
125129
alt={`${profile.name || profile.id} avatar`}
126130
className="mt-1 w-10 h-10 rounded-full flex-shrink-0 object-cover"
131+
width={40}
132+
height={40}
127133
onError={(e) => {
128134
// Fallback to blockie if image fails to load
129135
e.currentTarget.src = makeBlockie(profile.id);
@@ -133,10 +139,12 @@ export function ProfileSearch({ onSelectAddress }: SearchProps) {
133139
}
134140

135141
return (
136-
<img
142+
<Image
137143
src={makeBlockie(profile.id)}
138144
alt={`${profile.name || profile.id} avatar`}
139145
className="w-10 h-10 rounded-full flex-shrink-0"
146+
width={40}
147+
height={40}
140148
/>
141149
);
142150
};
@@ -210,7 +218,7 @@ export function ProfileSearch({ onSelectAddress }: SearchProps) {
210218
address={result.id}
211219
max-width="200"
212220
size="large"
213-
slice-by="8"
221+
slice-by="4"
214222
address-color=""
215223
name-color=""
216224
custom-class=""

0 commit comments

Comments
 (0)