├── .eslintrc.json ├── src ├── app │ ├── favicon.ico │ ├── contract │ │ └── page.tsx │ ├── layout.tsx │ ├── globals.css │ └── page.tsx ├── constants │ ├── index.ts │ └── abi.ts ├── assets │ ├── botfather-tut1.png │ └── Arrow.tsx ├── lib │ └── utils.ts ├── containers │ ├── home │ │ └── Profile.tsx │ └── contract │ │ ├── ReadContract.tsx │ │ └── WriteContract.tsx ├── hooks │ ├── useClientOnce.ts │ ├── useDidMount.ts │ └── useTelegramMock.ts ├── components │ ├── ErrorPage.tsx │ ├── ui │ │ ├── input.tsx │ │ ├── sonner.tsx │ │ └── button.tsx │ ├── ErrorBoundary.tsx │ └── shared │ │ └── Navbar.tsx ├── providers │ ├── Web3Provider.tsx │ ├── Layout.tsx │ └── TelegramProvider.tsx └── utils │ └── config.ts ├── postcss.config.js ├── .env.example ├── next.config.js ├── components.json ├── .gitignore ├── tsconfig.json ├── package.json ├── public ├── arrow.svg ├── butterfly.svg ├── arbitrum.svg ├── loader.svg └── rabble.svg ├── tailwind.config.ts └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HAPPYS1NGH/tg-mini-app-nextjs/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const counterAddress = "0x75b167a1628b6F239C0BEA211d731578bA90465a" 2 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/assets/botfather-tut1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HAPPYS1NGH/tg-mini-app-nextjs/HEAD/src/assets/botfather-tut1.png -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID='' # Required - https://cloud.walletconnect.com/ 2 | NEXT_PUBLIC_ENABLE_TESTNETS='' # true or false 3 | NODE_ENV='' # development or production -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/containers/home/Profile.tsx: -------------------------------------------------------------------------------- 1 | import { ConnectButton } from "@rainbow-me/rainbowkit" 2 | 3 | export default function Profile() { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /src/hooks/useClientOnce.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | export function useClientOnce(fn: () => void): void { 4 | const canCall = useRef(true); 5 | if (typeof window !== 'undefined' && canCall.current) { 6 | canCall.current = false; 7 | fn(); 8 | } 9 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | webpack: (config) => { 5 | config.externals.push("pino-pretty", "lokijs", "encoding") 6 | return config 7 | }, 8 | } 9 | 10 | module.exports = nextConfig 11 | -------------------------------------------------------------------------------- /src/hooks/useDidMount.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | /** 4 | * @return True, if component was mounted. 5 | */ 6 | export function useDidMount(): boolean { 7 | const [didMount, setDidMount] = useState(false); 8 | 9 | useEffect(() => { 10 | setDidMount(true); 11 | }, []); 12 | 13 | return didMount; 14 | } -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /src/components/ErrorPage.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | export function ErrorPage({ 4 | error, 5 | reset, 6 | }: { 7 | error: Error & { digest?: string } 8 | reset?: () => void 9 | }) { 10 | useEffect(() => { 11 | // Log the error to an error reporting service 12 | console.error(error); 13 | }, [error]); 14 | 15 | return ( 16 |
17 |

An unhandled error occurred!

18 |
19 | 20 | {error.message} 21 | 22 |
23 | {reset && } 24 |
25 | ); 26 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /src/app/contract/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { WriteContract } from "@/containers/contract/WriteContract"; 4 | import { ReadContract } from "@/containers/contract/ReadContract"; 5 | 6 | import { useAccount } from "wagmi"; 7 | function ContractExample() { 8 | const { isConnected } = useAccount(); 9 | return ( 10 |
11 | {isConnected ? ( 12 | <> 13 | 14 | 15 | 16 | ) : ( 17 |
18 | Please Connect the Wallet 19 |
20 | )} 21 |
22 | ); 23 | } 24 | 25 | export default ContractExample; 26 | -------------------------------------------------------------------------------- /src/constants/abi.ts: -------------------------------------------------------------------------------- 1 | export const counterAbi = [ 2 | { 3 | inputs: [], 4 | name: "number", 5 | outputs: [ 6 | { 7 | internalType: "uint256", 8 | name: "", 9 | type: "uint256", 10 | }, 11 | ], 12 | stateMutability: "view", 13 | type: "function", 14 | }, 15 | { 16 | inputs: [ 17 | { 18 | internalType: "uint256", 19 | name: "new_number", 20 | type: "uint256", 21 | }, 22 | ], 23 | name: "setNumber", 24 | outputs: [], 25 | stateMutability: "external", 26 | type: "function", 27 | }, 28 | { 29 | inputs: [], 30 | name: "increment", 31 | outputs: [], 32 | stateMutability: "external", 33 | type: "function", 34 | }, 35 | ] 36 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /src/containers/contract/ReadContract.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useReadContract } from "wagmi"; 3 | import { counterAbi } from "@/constants/abi"; 4 | import { counterAddress } from "@/constants"; 5 | 6 | export function ReadContract() { 7 | const { 8 | data: counter, 9 | status, 10 | isLoading, 11 | error, 12 | } = useReadContract({ 13 | abi: counterAbi, 14 | address: counterAddress, 15 | functionName: "number", 16 | }); 17 | 18 | console.log(counter, status, isLoading, error); 19 | 20 | return ( 21 |
22 | {isLoading ? ( 23 |
Loading
24 | ) : error ? ( 25 |
Error
26 | ) : ( 27 |
28 | Current Number:{" "} 29 | {counter?.toString()} 30 |
31 | )} 32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/providers/Web3Provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { WagmiProvider } from "wagmi"; 3 | import { 4 | darkTheme, 5 | lightTheme, 6 | RainbowKitProvider, 7 | } from "@rainbow-me/rainbowkit"; 8 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 9 | import { config } from "../utils/config"; 10 | 11 | const queryClient = new QueryClient(); 12 | 13 | export default function Web3Provider({ 14 | children, 15 | }: { 16 | children: React.ReactNode; 17 | }) { 18 | return ( 19 | 20 | 21 | 29 | {children} 30 | 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useTheme } from "next-themes" 4 | import { Toaster as Sonner } from "sonner" 5 | 6 | type ToasterProps = React.ComponentProps 7 | 8 | const Toaster = ({ ...props }: ToasterProps) => { 9 | const { theme = "system" } = useTheme() 10 | 11 | return ( 12 | 28 | ) 29 | } 30 | 31 | export { Toaster } 32 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | type ComponentType, 4 | type GetDerivedStateFromError, 5 | type PropsWithChildren, 6 | } from 'react'; 7 | 8 | export interface ErrorBoundaryProps extends PropsWithChildren { 9 | fallback: ComponentType<{ error: Error }>; 10 | } 11 | 12 | interface ErrorBoundaryState { 13 | error?: Error; 14 | } 15 | 16 | export class ErrorBoundary extends Component { 17 | state: ErrorBoundaryState = {}; 18 | 19 | // eslint-disable-next-line max-len 20 | static getDerivedStateFromError: GetDerivedStateFromError = (error) => ({ error }); 21 | 22 | componentDidCatch(error: Error) { 23 | this.setState({ error }); 24 | } 25 | 26 | render() { 27 | const { 28 | state: { 29 | error, 30 | }, 31 | props: { 32 | fallback: Fallback, 33 | children, 34 | }, 35 | } = this; 36 | 37 | return error ? : children; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/utils/config.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultWallets, getDefaultConfig } from "@rainbow-me/rainbowkit" 2 | import { 3 | argentWallet, 4 | trustWallet, 5 | ledgerWallet, 6 | } from "@rainbow-me/rainbowkit/wallets" 7 | import { 8 | arbitrum, 9 | arbitrumSepolia, 10 | localhost, 11 | mainnet, 12 | } from "wagmi/chains" 13 | 14 | const { wallets } = getDefaultWallets() 15 | 16 | export const WALLETCONNECT_PROJECT_ID = 17 | process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID ?? "" 18 | if (!WALLETCONNECT_PROJECT_ID) { 19 | console.warn( 20 | "You need to provide a NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID env variable" 21 | ) 22 | } 23 | export const config = getDefaultConfig({ 24 | appName: "RainbowKit demo", 25 | projectId: WALLETCONNECT_PROJECT_ID, 26 | wallets: [ 27 | ...wallets, 28 | { 29 | groupName: "Other", 30 | wallets: [argentWallet, trustWallet, ledgerWallet], 31 | }, 32 | ], 33 | chains: [ 34 | mainnet, 35 | ...(process.env.NEXT_PUBLIC_ENABLE_TESTNETS === "true" 36 | ? [arbitrumSepolia, arbitrum, localhost] 37 | : []), 38 | ], 39 | ssr: true, 40 | }) 41 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Outfit, Be_Vietnam_Pro } from "next/font/google"; 3 | import "@rainbow-me/rainbowkit/styles.css"; 4 | import "./globals.css"; 5 | import Web3Provider from "@/providers/Web3Provider"; 6 | import Layout from "@/providers/Layout"; 7 | import { TelegramProvider } from "@/providers/TelegramProvider"; 8 | 9 | const outfit = Outfit({ subsets: ["latin"], variable: "--font-outfit" }); 10 | const beVietnamPro = Be_Vietnam_Pro({ 11 | weight: "400", 12 | subsets: ["latin"], 13 | variable: "--font-beVietnamPro", 14 | }); 15 | 16 | export const metadata: Metadata = { 17 | title: "Create ETH Mini App", 18 | description: "Template for creating a Mini App on Ethereum", 19 | }; 20 | 21 | export default function RootLayout({ 22 | children, 23 | }: { 24 | children: React.ReactNode; 25 | }) { 26 | return ( 27 | 28 | 29 | 30 | 31 | {children} 32 | 33 | 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "happy-starter-kit", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "expose": "lt --port 3000" 11 | }, 12 | "dependencies": { 13 | "@radix-ui/react-slot": "^1.0.2", 14 | "@rainbow-me/rainbowkit": "^2.0.2", 15 | "@tanstack/react-query": "^5.27.5", 16 | "@telegram-apps/sdk-react": "^1.1.3", 17 | "class-variance-authority": "^0.7.0", 18 | "clsx": "^2.1.0", 19 | "eruda": "^3.2.1", 20 | "localtunnel": "^2.0.2", 21 | "lucide-react": "^0.356.0", 22 | "next": "14.2.4", 23 | "next-themes": "^0.3.0", 24 | "react": "^18", 25 | "react-dom": "^18", 26 | "sonner": "^1.4.3", 27 | "tailwind-merge": "^2.2.1", 28 | "tailwindcss-animate": "^1.0.7", 29 | "viem": "^2.8.4", 30 | "wagmi": "^2.5.7" 31 | }, 32 | "devDependencies": { 33 | "@types/node": "^20", 34 | "@types/react": "^18", 35 | "@types/react-dom": "^18", 36 | "autoprefixer": "^10", 37 | "eslint": "^8", 38 | "eslint-config-next": "13.5.6", 39 | "postcss": "^8", 40 | "tailwindcss": "^3", 41 | "typescript": "^5" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /public/arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/butterfly.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/providers/Layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useEffect } from "react"; 3 | import Navbar from "@/components/shared/Navbar"; 4 | import { Toaster } from "@/components/ui/sonner"; 5 | import { 6 | useBackButton, 7 | useClosingBehavior, 8 | useViewport, 9 | } from "@telegram-apps/sdk-react"; 10 | import { useRouter, usePathname } from "next/navigation"; 11 | 12 | function Layout({ children }: { children: React.ReactNode }) { 13 | const bb = useBackButton(); 14 | const close = useClosingBehavior(); // will be undefined or ClosingBehavior. 15 | const viewport = useViewport(); // will be undefined or InitData. 16 | const router = useRouter(); 17 | const pathname = usePathname(); 18 | useEffect(() => { 19 | function goBack() { 20 | router.back(); 21 | } 22 | if (close) { 23 | close.enableConfirmation(); 24 | } 25 | if (viewport) { 26 | viewport.expand(); 27 | } 28 | if (bb) { 29 | if (pathname === "/") { 30 | bb.hide(); 31 | return; 32 | } 33 | bb.show(); 34 | bb.on("click", goBack); 35 | } 36 | }, [bb, router, pathname]); 37 | 38 | return ( 39 |
40 | 41 |
{children}
42 | 43 |
44 | ); 45 | } 46 | 47 | export default Layout; 48 | -------------------------------------------------------------------------------- /src/components/shared/Navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import Link from "next/link"; 4 | import { ConnectButton } from "@rainbow-me/rainbowkit"; 5 | import { usePathname } from "next/navigation"; 6 | import Image from "next/image"; 7 | 8 | const Navbar = () => { 9 | const pathname = usePathname(); 10 | console.log(pathname); 11 | 12 | return ( 13 | 42 | ); 43 | }; 44 | 45 | export default Navbar; 46 | -------------------------------------------------------------------------------- /src/assets/Arrow.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Arrow = ({ color }: { color: string }) => ( 4 | 11 | 15 | 16 | ); 17 | 18 | export default Arrow; 19 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } 77 | 78 | h1, 79 | h2, 80 | h3, 81 | h4, 82 | h5, 83 | h6 { 84 | font-family: sans-serif; 85 | } 86 | -------------------------------------------------------------------------------- /src/containers/contract/WriteContract.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import * as React from "react"; 3 | import { useEffect } from "react"; 4 | import { Input } from "@/components/ui/input"; 5 | import { Button } from "@/components/ui/button"; 6 | import { useWaitForTransactionReceipt, useWriteContract } from "wagmi"; 7 | import { toast } from "sonner"; 8 | 9 | import { counterAbi } from "@/constants/abi"; 10 | import { counterAddress } from "@/constants"; 11 | 12 | export function WriteContract() { 13 | const { data: hash, isPending, writeContract } = useWriteContract(); 14 | 15 | async function submit(e: React.FormEvent) { 16 | e.preventDefault(); 17 | const formData = new FormData(e.target as HTMLFormElement); 18 | const tokenId = formData.get("value") as string; 19 | console.log(tokenId); 20 | writeContract({ 21 | address: counterAddress, 22 | abi: counterAbi, 23 | functionName: "setNumber", 24 | args: [BigInt(tokenId)], 25 | }); 26 | } 27 | 28 | const { 29 | isLoading: isConfirming, 30 | error, 31 | isSuccess: isConfirmed, 32 | } = useWaitForTransactionReceipt({ 33 | hash, 34 | }); 35 | 36 | useEffect(() => { 37 | if (isConfirmed) { 38 | toast.success("Transaction Successful"); 39 | } 40 | if (error) { 41 | toast.error("Transaction Failed"); 42 | } 43 | }, [isConfirmed, error]); 44 | 45 | return ( 46 |
47 |

48 | Make this counter your favorite number 49 |

50 |
51 | 58 | 66 |
67 |
68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /public/arbitrum.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 24 | 25 | 26 | 27 | 32 | 33 | 34 | 36 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | rabble: "bg-rabble text-primary-foreground hover:bg-rabble/90 text-md", 22 | tertiary: 23 | "bg-white text-primary-foreground hover:bg-[#E7E7E7]/90 text-md text-[#DC15BA]", 24 | }, 25 | size: { 26 | default: "h-10 px-4 py-2", 27 | sm: "h-9 rounded-md px-3", 28 | lg: "h-11 rounded-md px-8", 29 | icon: "h-10 w-10", 30 | half: "w-1/2 px-4 py-1 rounded-full", 31 | "one-third": "w-1/3 px-4 py-1 rounded-full", 32 | }, 33 | }, 34 | defaultVariants: { 35 | variant: "default", 36 | size: "default", 37 | }, 38 | } 39 | ); 40 | 41 | export interface ButtonProps 42 | extends React.ButtonHTMLAttributes, 43 | VariantProps { 44 | asChild?: boolean; 45 | } 46 | 47 | const Button = React.forwardRef( 48 | ({ className, variant, size, asChild = false, ...props }, ref) => { 49 | const Comp = asChild ? Slot : "button"; 50 | return ( 51 | 56 | ); 57 | } 58 | ); 59 | Button.displayName = "Button"; 60 | 61 | export { Button, buttonVariants }; 62 | -------------------------------------------------------------------------------- /src/providers/TelegramProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { type PropsWithChildren, useEffect, useMemo, useState } from "react"; 4 | import { SDKProvider, useLaunchParams } from "@telegram-apps/sdk-react"; 5 | 6 | import { ErrorBoundary } from "@/components/ErrorBoundary"; 7 | import { ErrorPage } from "@/components/ErrorPage"; 8 | import { useTelegramMock } from "@/hooks/useTelegramMock"; 9 | import { useDidMount } from "@/hooks/useDidMount"; 10 | import Image from "next/image"; 11 | 12 | // function App(props: PropsWithChildren) { 13 | // const lp = useLaunchParams(); 14 | // const miniApp = useMiniApp(); 15 | // const themeParams = useThemeParams(); 16 | // const viewport = useViewport(); 17 | 18 | // useEffect(() => { 19 | // return bindMiniAppCSSVars(miniApp, themeParams); 20 | // }, [miniApp, themeParams]); 21 | 22 | // useEffect(() => { 23 | // return bindThemeParamsCSSVars(themeParams); 24 | // }, [themeParams]); 25 | 26 | // useEffect(() => { 27 | // return viewport && bindViewportCSSVars(viewport); 28 | // }, [viewport]); 29 | 30 | // return ( 31 | // < 32 | // > 33 | // {props.children} 34 | // 35 | // ); 36 | // } 37 | 38 | function RootInner({ children }: PropsWithChildren) { 39 | // Mock Telegram environment in development mode if needed. 40 | if (process.env.NODE_ENV === "development") { 41 | // eslint-disable-next-line react-hooks/rules-of-hooks 42 | useTelegramMock(); 43 | } 44 | 45 | const debug = useLaunchParams().startParam === "debug"; 46 | 47 | // Enable debug mode to see all the methods sent and events received. 48 | useEffect(() => { 49 | if (debug) { 50 | import("eruda").then((lib) => lib.default.init()); 51 | } 52 | }, [debug]); 53 | 54 | return ( 55 | 56 | {children} 57 | 58 | ); 59 | } 60 | 61 | export function TelegramProvider(props: PropsWithChildren) { 62 | // Unfortunately, Telegram Mini Apps does not allow us to use all features of the Server Side 63 | // Rendering. That's why we are showing loader on the server side. 64 | const didMount = useDidMount(); 65 | 66 | return didMount ? ( 67 | 68 | 69 | 70 | ) : ( 71 |
72 | Rabble 73 |
74 |

Loading

75 | loader 82 |
83 |
84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss" 2 | 3 | const config = { 4 | darkMode: ["class"], 5 | content: [ 6 | './pages/**/*.{ts,tsx}', 7 | './components/**/*.{ts,tsx}', 8 | './app/**/*.{ts,tsx}', 9 | './src/**/*.{ts,tsx}', 10 | ], 11 | prefix: "", 12 | theme: { 13 | container: { 14 | center: true, 15 | padding: "2rem", 16 | screens: { 17 | "2xl": "1400px", 18 | }, 19 | }, 20 | extend: { 21 | colors: { 22 | rabble: "#08F7AF", 23 | back: "#E7E7E7", 24 | border: "hsl(var(--border))", 25 | input: "hsl(var(--input))", 26 | ring: "hsl(var(--ring))", 27 | background: "var(--tg-theme-bg-color)", 28 | "secondary-background": "var(--tg-theme-secondary-bg-color)", 29 | // var(--tg-theme-bg-color) 30 | // var(--tg-theme-text-color) 31 | // var(--tg-theme-hint-color) 32 | // var(--tg-theme-link-color) 33 | // var(--tg-theme-button-color) 34 | // var(--tg-theme-button-text-color) 35 | // 36 | color: "var(--tg-theme-text-color)", 37 | "subtitle-color": "var(--tg-theme-subtitle-text-color)", 38 | foreground: "hsl(var(--foreground))", 39 | primary: { 40 | DEFAULT: "hsl(var(--primary))", 41 | foreground: "hsl(var(--primary-foreground))", 42 | }, 43 | secondary: { 44 | DEFAULT: "hsl(var(--secondary))", 45 | foreground: "hsl(var(--secondary-foreground))", 46 | }, 47 | destructive: { 48 | DEFAULT: "hsl(var(--destructive))", 49 | foreground: "hsl(var(--destructive-foreground))", 50 | }, 51 | muted: { 52 | DEFAULT: "hsl(var(--muted))", 53 | foreground: "hsl(var(--muted-foreground))", 54 | }, 55 | accent: { 56 | DEFAULT: "hsl(var(--accent))", 57 | foreground: "hsl(var(--accent-foreground))", 58 | }, 59 | popover: { 60 | DEFAULT: "hsl(var(--popover))", 61 | foreground: "hsl(var(--popover-foreground))", 62 | }, 63 | card: { 64 | DEFAULT: "hsl(var(--card))", 65 | foreground: "hsl(var(--card-foreground))", 66 | }, 67 | }, 68 | borderRadius: { 69 | lg: "var(--radius)", 70 | md: "calc(var(--radius) - 2px)", 71 | sm: "calc(var(--radius) - 4px)", 72 | }, 73 | keyframes: { 74 | "accordion-down": { 75 | from: { height: "0" }, 76 | to: { height: "var(--radix-accordion-content-height)" }, 77 | }, 78 | "accordion-up": { 79 | from: { height: "var(--radix-accordion-content-height)" }, 80 | to: { height: "0" }, 81 | }, 82 | }, 83 | animation: { 84 | "accordion-down": "accordion-down 0.2s ease-out", 85 | "accordion-up": "accordion-up 0.2s ease-out", 86 | }, 87 | fontFamily: { 88 | sans: ['var(--font-outfit)'], 89 | mono: ['var(--font-beVietnamPro)'], 90 | }, 91 | }, 92 | }, 93 | plugins: [require("tailwindcss-animate")], 94 | } satisfies Config 95 | 96 | export default config -------------------------------------------------------------------------------- /src/hooks/useTelegramMock.ts: -------------------------------------------------------------------------------- 1 | import { useClientOnce } from '@/hooks/useClientOnce'; 2 | import { mockTelegramEnv, parseInitData, retrieveLaunchParams } from '@telegram-apps/sdk-react'; 3 | 4 | /** 5 | * Mocks Telegram environment in development mode. 6 | */ 7 | export function useTelegramMock(): void { 8 | useClientOnce(() => { 9 | // It is important, to mock the environment only for development purposes. When building the 10 | // application, import.meta.env.DEV will become false, and the code inside will be tree-shaken, 11 | // so you will not see it in your final bundle. 12 | 13 | let shouldMock: boolean; 14 | 15 | // Try to extract launch parameters to check if the current environment is Telegram-based. 16 | try { 17 | // If we are able to extract launch parameters, it means that we are already in the 18 | // Telegram environment. So, there is no need to mock it. 19 | retrieveLaunchParams(); 20 | 21 | // We could previously mock the environment. In case we did, we should do it again. The reason 22 | // is the page could be reloaded, and we should apply mock again, because mocking also 23 | // enables modifying the window object. 24 | shouldMock = !!sessionStorage.getItem('____mocked'); 25 | } catch (e) { 26 | shouldMock = true; 27 | } 28 | 29 | if (shouldMock) { 30 | const initDataRaw = new URLSearchParams([ 31 | ['user', JSON.stringify({ 32 | id: 99281932, 33 | first_name: 'Andrew', 34 | last_name: 'Rogue', 35 | username: 'rogue', 36 | language_code: 'en', 37 | is_premium: true, 38 | allows_write_to_pm: true, 39 | })], 40 | ['hash', '89d6079ad6762351f38c6dbbc41bb53048019256a9443988af7a48bcad16ba31'], 41 | ['auth_date', '1716922846'], 42 | ['start_param', 'debug'], 43 | ['chat_type', 'sender'], 44 | ['chat_instance', '8428209589180549439'], 45 | ]).toString(); 46 | 47 | mockTelegramEnv({ 48 | themeParams: { 49 | accentTextColor: '#6ab2f2', 50 | bgColor: '#17212b', 51 | buttonColor: '#5288c1', 52 | buttonTextColor: '#ffffff', 53 | destructiveTextColor: '#ec3942', 54 | headerBgColor: '#17212b', 55 | hintColor: '#708499', 56 | linkColor: '#6ab3f3', 57 | secondaryBgColor: '#232e3c', 58 | sectionBgColor: '#17212b', 59 | sectionHeaderTextColor: '#6ab3f3', 60 | subtitleTextColor: '#708499', 61 | textColor: '#f5f5f5', 62 | }, 63 | initData: parseInitData(initDataRaw), 64 | initDataRaw, 65 | version: '7.2', 66 | platform: 'tdesktop', 67 | }); 68 | sessionStorage.setItem('____mocked', '1'); 69 | 70 | console.info( 71 | 'As long as the current environment was not considered as the Telegram-based one, it was mocked. Take a note, that you should not do it in production and current behavior is only specific to the development process. Environment mocking is also applied only in development mode. So, after building the application, you will not see this behavior and related warning, leading to crashing the application outside Telegram.', 72 | ); 73 | } 74 | }); 75 | } -------------------------------------------------------------------------------- /public/loader.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | useUtils, 5 | usePopup, 6 | useMainButton, 7 | useViewport, 8 | } from "@telegram-apps/sdk-react"; 9 | import Link from "next/link"; 10 | import Arrow from "@/assets/Arrow"; 11 | import { Button } from "@/components/ui/button"; 12 | 13 | export default function Home() { 14 | const utils = useUtils(); 15 | const popUp = usePopup(); 16 | const mainBtn = useMainButton(); 17 | const viewport = useViewport(); 18 | 19 | const handlePopUp = async () => { 20 | const response = await popUp.open({ 21 | title: " Rabble", 22 | message: "Link will lead to website", 23 | buttons: [ 24 | { id: "link", type: "default", text: "Open rabble.pro" }, 25 | { type: "cancel" }, 26 | ], 27 | }); 28 | if (response === "link") { 29 | utils.openLink("https://rabble.pro"); 30 | } 31 | }; 32 | 33 | const handleShare = async () => { 34 | utils.shareURL( 35 | "https://t.me/+rFqLyk4_W-diZDZl", 36 | "Join! Mini Apps Hackathon group!" 37 | ); 38 | }; 39 | const handleMainBtn = async () => { 40 | mainBtn.enable(); 41 | mainBtn.setText("New Text"); 42 | mainBtn.setBgColor("#08F7AF"); 43 | if (mainBtn.isVisible) { 44 | mainBtn.hide(); 45 | } else { 46 | mainBtn.show(); 47 | } 48 | }; 49 | 50 | mainBtn.on("click", () => { 51 | mainBtn.showLoader(); 52 | mainBtn.setText("Action Performing"); 53 | setTimeout(() => { 54 | console.log("Main Button Clicked"); 55 | mainBtn.hideLoader(); 56 | mainBtn.setText("New Text"); 57 | mainBtn.hide(); 58 | }, 2000); 59 | }); 60 | 61 | const handleViewport = async () => { 62 | if (!viewport?.isExpanded) { 63 | viewport?.expand(); 64 | } 65 | }; 66 | return ( 67 |
68 |

69 | Telegram Miniapp Boilerplate 70 |

71 |
72 |
73 | 74 |
75 |

Docs

76 | 77 |
78 |

79 | Find in-depth information on making telegram miniapps for Rabble. 80 |

81 | 82 |
83 |
84 |

Test Modals

85 |

Click to see how modals work

86 |
87 | 90 | 93 |
94 |
95 |
96 |

Test Buttons

97 |

Click to see how buttons work

98 |
99 | 102 | 105 |
106 |
107 |
108 |
109 | ); 110 | } 111 | -------------------------------------------------------------------------------- /public/rabble.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ETH Telegram Mini App Starter Kit 2 | 3 | ## Getting Started 4 | 5 | This starter kit helps you create a mini application integrated with Ethereum and Telegram. Follow the steps below to set up and run the project. 6 | 7 | ## Resources 8 | 9 | [Blog on using this Starter Kit](https://medium.com/rabble-labs/deploy-your-first-telegram-mini-app-on-ethereum-8b589f4e6411) 10 | 11 | **For Visual Learners** 12 | [![YouTube](http://i.ytimg.com/vi/cFLKu4sl76I/hqdefault.jpg)](https://www.youtube.com/watch?v=cFLKu4sl76I) 13 | 14 | ### Prerequisites 15 | 16 | Ensure you have the following installed on your machine: 17 | 18 | - [Node.js](https://nodejs.org/) (v16 or higher) 19 | - [npm](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) 20 | 21 | ### Setup Guide 22 | 23 | 1. **Clone the repository or click on the "Use this template" button:** 24 | 25 | ```bash 26 | git clone https://github.com/HAPPYS1NGH/tg-mini-app-nextjs 27 | ``` 28 | 29 | 2. **Navigate to the project directory:** 30 | 31 | ```bash 32 | cd tg-mini-app-nextjs 33 | ``` 34 | 35 | 3. **Create a `.env.local` file in the root directory and copy the contents of `.env.sample`:** 36 | 37 | - Obtain the WalletConnect project ID from [Reown](https://cloud.reown.com/). 38 | 39 | - Make sure to select the App Kit. 40 | 41 | ```env 42 | NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID= 43 | ``` 44 | 45 | - According change the other environment details to development or production. 46 | 47 | 4. **Install dependencies:** 48 | 49 | Using npm: 50 | 51 | ```bash 52 | npm install 53 | ``` 54 | 55 | or using yarn: 56 | 57 | ```bash 58 | yarn install 59 | ``` 60 | 61 | 5. **Start the development server:** 62 | 63 | Using npm: 64 | 65 | ```bash 66 | npm run dev 67 | ``` 68 | 69 | or using yarn: 70 | 71 | ```bash 72 | yarn dev 73 | ``` 74 | 75 | 6. **Open your web browser and navigate to `http://localhost:3000` to view the application.** 76 | 77 | ### Exposing Your Local Server 78 | 79 | To test your application within Telegram, you need to expose your local server using a tunneling service like ngrok or localtunnel. 80 | 81 | **Start the development server:** 82 | 83 | Using npm: 84 | 85 | ```bash 86 | npm run expose 87 | ``` 88 | 89 | or using yarn: 90 | 91 | ```bash 92 | yarn expose 93 | ``` 94 | 95 | **Alternatively:** 96 | 97 | You can always use ngrok or any proxy service to expose the endpoint. 98 | 99 | ### Registering Your Bot on Telegram while Development 100 | 101 | 1. **Open Telegram and search for `BotFather`.** 102 | 103 | 2. **Register a new bot by using the /newbot command and follow the prompts to choose a name and username.** 104 | 105 | 3. **While in BotFather, use the /setmenubutton command (It may not autocomplete).** 106 | 107 | 4. **Click on the bottom right square to choose the bot** 108 | ![Selecting the Bot](/src/assets/botfather-tut1.png?raw=true) 109 | 110 | 5. **Paste the URL for your App in which will be LocalTunnel's during development.** 111 | 112 | 6. **Set the name of the button which will be used to start the mini app.** 113 | 114 | 7. **Go to the Bot and now you can see a small button next to the chat which will bring up the Mini App.** 115 | 116 | 8. **Repeat the steps 3-7 when you have the production URL.** 117 | 118 | --- 119 | 120 | ### Registering Your Bot on Telegram on Production 121 | 122 | 0. **Deploy your App on any Platform like Vercel or Netlify** 123 | 124 | 1. **Open Telegram and search for `BotFather`.** 125 | 126 | 2. **Register a new bot by using the /newbot command and follow the prompts to choose a name and username.** 127 | 128 | 3. **While in BotFather, use the /setmenubutton command (It may not autocomplete).** 129 | 130 | 4. **Click on the bottom right square to choose the bot** 131 | ![Selecting the Bot](/src/assets/botfather-tut1.png?raw=true) 132 | 133 | 5. **Paste the Production URL for your App.** 134 | 135 | 6. **Set the name of the button which will be used to start the mini app.** 136 | 137 | 7. **Go to the Bot and now you can see a small button next to the chat which will bring up the Mini App.** 138 | 139 | 8. **Again go to BotFather, use the /setminiapp command (It may not autocomplete) and choose your Bot** 140 | 141 | 9. **Paste the Production URL for your Mini App.** 142 | 143 | 10. **Now you are all set for sharing your Mini App.** 144 | 145 | ## Interacting with Contracts 146 | 147 | This starter kit provides hooks from Wagmi v2 for interacting with smart contracts on the Arbitrum network. You can use these hooks to read and write data to contracts. 148 | 149 | ## Directory Structure 150 | 151 | The project follows a standard directory structure for a Next.js application. Here's an overview: 152 | 153 | ``` 154 | . 155 | ├── README.md 156 | ├── components.json 157 | ├── next-env.d.ts 158 | ├── next.config.js 159 | ├── package-lock.json 160 | ├── package.json 161 | ├── postcss.config.js 162 | ├── public 163 | │ ├── arbitrum.svg 164 | │ ├── arrow.svg 165 | │ ├── butterfly.svg 166 | │ ├── loader.svg 167 | │ └── rabble.svg 168 | ├── src 169 | │ ├── app 170 | │ │ ├── contract 171 | │ │ │ └── page.tsx 172 | │ │ ├── favicon.ico 173 | │ │ ├── globals.css 174 | │ │ ├── layout.tsx 175 | │ │ └── page.tsx 176 | │ ├── assets 177 | │ │ └── Arrow.tsx 178 | │ ├── components 179 | │ │ ├── ErrorBoundary.tsx 180 | │ │ ├── ErrorPage.tsx 181 | │ │ ├── Popup.tsx 182 | │ │ ├── shared 183 | │ │ │ └── Navbar.tsx 184 | │ │ └── ui 185 | │ │ ├── button.tsx 186 | │ │ ├── input.tsx 187 | │ │ └── sonner.tsx 188 | │ ├── constants 189 | │ │ ├── abi.ts 190 | │ │ └── index.ts 191 | │ ├── containers 192 | │ │ ├── contract 193 | │ │ │ ├── ReadContract.tsx 194 | │ │ │ └── WriteContract.tsx 195 | │ │ └── home 196 | │ │ └── Profile.tsx 197 | │ ├── hooks 198 | │ │ ├── useClientOnce.ts 199 | │ │ ├── useDidMount.ts 200 | │ │ └── useTelegramMock.ts 201 | │ ├── lib 202 | │ │ └── utils.ts 203 | │ ├── providers 204 | │ │ ├── Layout.tsx 205 | │ │ ├── TelegramProvider.tsx 206 | │ │ └── Web3Provider.tsx 207 | │ └── utils 208 | │ └── config.ts 209 | ├── tailwind.config.ts 210 | └── tsconfig.json 211 | ``` 212 | 213 | ## FAQs 214 | 215 | ### What are Telegram Mini Apps? 216 | 217 | Web Apps inside Telegram in the form of a bot. 218 | 219 | ### What is different in Mini Apps? 220 | 221 | Mini Apps offer Telegram-specific UI elements like Main Button, Popups, Telegram Theme Params, and Viewport. They also provide features like Telegram Authentication, Cloud Storage, and more. 222 | 223 | ### Can you tell what all things I need to do to convert my WebApp to a Mini App? 224 | 225 | Your normal website will also work fine in most cases if you do not have in-app links to other domains. 226 | 227 | ## Support 228 | 229 | If you encounter any issues or have questions: 230 | 231 | - **Telegram:** [Rabble Mini App Group](https://t.me/+rFqLyk4_W-diZDZl) 232 | - **Twitter:** [@happys1ngh](https://twitter.com/happys1ngh) 233 | - **GitHub Issues:** [ETH Telegram Mini App Starter Kit Issues](https://github.com/HAPPYS1NGH/tg-mini-app-nextjs/issues) 234 | - **Mini Apps Hackathon:** [MAHa](https://0xmaha.com) 235 | 236 | BUIDL SHOULD NOT STOP!🏗️ 237 | 238 | Happy coding! 🚀 239 | --------------------------------------------------------------------------------