Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Telegram Mini App integration #2

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
ANALYZE=false # true | false
# Set the Node.js environment (production: for live deployment, development: for local development)
NODE_ENV=production

NODE_ENV=production # production | development
# Enable or disable bundle analysis (true/false)
ANALYZE=false

# Open bundle analyzer automatically after build (true/false)
OPEN_ANALYZER=false

# Set the mode for bundle analyzer output (static: HTML file, json: JSON file)
ANALYZER_MODE=static
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

# testing
/coverage
/analyze

# next.js
/.next/
Expand Down
7 changes: 4 additions & 3 deletions next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import bundleAnalyzer from '@next/bundle-analyzer'
import { env } from './src/env.mjs'

const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
openAnalyzer: false,
analyzerMode: 'static',
enabled: env.ANALYZE,
openAnalyzer: env.OPEN_ANALYZER,
analyzerMode: env.ANALYZER_MODE,
})

/**
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,16 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwindcss": "^3.4.14",
"typescript": "^5.6.3"
"typescript": "^5.6.3",
"zod": "^3.23.8"
},
"devDependencies": {
"@eslint/compat": "^1.2.1",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.13.0",
"@next/bundle-analyzer": "^14.2.15",
"@next/eslint-plugin-next": "^14.2.15",
"@t3-oss/env-nextjs": "^0.11.1",
"@types/node": "^22.7.6",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
Expand Down
36 changes: 36 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* This file exports all components from the #components directory.
* It serves as a central point for importing components throughout the application.
* When adding new components, make sure to export them here for easy access.
*
* Example usage:
* Instead of:
* import { Button } from '#components/Button'
* import { Input } from '#components/Input'
*
* You can now use:
* import { Button, Input } from '#components'
*/
46 changes: 46 additions & 0 deletions src/env.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { createEnv } from '@t3-oss/env-nextjs'
import { z } from 'zod'

export const env = createEnv({
/*
* Server-side Environment variables, not available on the client.
* Will throw if you access these variables on the client.
*/
server: {
NODE_ENV: z.enum(['development', 'production']),

ANALYZE: z.string().transform(v => v === 'true'),
OPEN_ANALYZER: z.string().transform(v => v === 'true'),
ANALYZER_MODE: z.enum(['static', 'json']),
},

/*
* Environment variables available on the client (and server).
*/
client: {
//
},

/*
* Due to how Next.js bundles environment variables on Edge and Client,
* we need to manually destructure them to make sure all are included in bundle.
*/
runtimeEnv: {
NODE_ENV: process.env.NODE_ENV,

ANALYZE: process.env.ANALYZE,
OPEN_ANALYZER: process.env.OPEN_ANALYZER,
ANALYZER_MODE: process.env.ANALYZER_MODE,
},

/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.
* This is especially useful for Docker builds.
*/
skipValidation: false,
mtakhirov marked this conversation as resolved.
Show resolved Hide resolved
/**
* Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and
* `SOME_VAR=''` will throw an error.
*/
emptyStringAsUndefined: true,
})
16 changes: 16 additions & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* This file exports all functions from the #hooks directory.
* It serves as a central point for importing hook functions throughout the application.
* When adding new hooks, make sure to export them here for easy access.
*
* Example usage:
* Instead of:
* import { useDidMount } from '#hooks/useDidMount'
* import { useSomeFn } from '#hooks/useSomeFn'
*
* You can now use:
* import { useDidMount, useSomeFn } from '#hooks'
*/

export { useClientOnce } from '#hooks/useClientOnce'
export { useDidMount } from '#hooks/useDidMount'
28 changes: 28 additions & 0 deletions src/hooks/useClientOnce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useEffect, useRef } from 'react'

type UseClientOnceFn = () => void

/**
* A custom React hook that ensures a function is called only once on the client side.
*
* @param {UseClientOnceFn} fn - The function to be executed once on the client side.
*
* @example ```ts
* useClientOnce(() => {
* console.log('This will be logged only once on the client side');
* });
* ```
* @see https://github.com/Telegram-Mini-Apps/telegram-apps/blob/master/playgrounds/next/src/hooks/useClientOnce.ts
*/
export function useClientOnce(fn: UseClientOnceFn): void {
const hasRun = useRef(false)

useEffect(() => {
if (!hasRun.current) {
hasRun.current = true
fn()
}
}, [fn])

return
}
24 changes: 24 additions & 0 deletions src/hooks/useDidMount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useEffect, useState } from 'react'

/**
* A custom React hook that determines if a component has mounted.
*
* @returns {boolean} true if the component has mounted, false otherwise.
*
* @example ```
* const isMounted = useDidMount();
* if (isMounted) {
* // Perform actions only after component has mounted
* }
* ```
* @see https://github.com/Telegram-Mini-Apps/telegram-apps/blob/master/playgrounds/next/src/hooks/useDidMount.ts
*/
export function useDidMount(): boolean {
const [didMount, setDidMount] = useState(false)

useEffect(() => {
setDidMount(true)
}, [])

return didMount
}
117 changes: 117 additions & 0 deletions src/hooks/useTelegramMock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { parseInitData, isTMA, mockTelegramEnv } from '@telegram-apps/sdk-react'
import { useClientOnce } from '#hooks'

/**
* A hook for simulating the Telegram environment during development.
* This hook only works on the client side and is only active in development mode.
*
* @returns {void}
* @see https://github.com/Telegram-Mini-Apps/telegram-apps/blob/master/playgrounds/next/src/hooks/useTelegramMock.ts
*/
export function useTelegramMock(): void {
useClientOnce(() => {
if (!shouldMockEnvironment()) {
return
}

const initDataRaw = createMockInitData()
applyMockEnvironment(initDataRaw)

logMockWarning()
})
}

/**
* Determines if the environment should be mocked.
*
* @returns {boolean} Whether the environment should be mocked or not
*/
function shouldMockEnvironment(): boolean {
const MOCK_KEY = '____mocked'

// Returns true if the current environment is Telegram Mini Apps.
// We don't mock if we are already in a mini app.
if (isTMA('simple')) {
// We could previously mock the environment.
// In case we did, we should do it again.
// The reason is the page could be reloaded, and we should apply mock again,
// because mocking also enables modifying the window object.
return !!sessionStorage.getItem(MOCK_KEY)
}

return true
}

/**
* Creates mocked initData.
*
* @returns {string} initData in URLSearchParams format
*/
function createMockInitData(): string {
return new URLSearchParams([
[
'user',
JSON.stringify({
id: 99281932,
first_name: 'Ryan',
last_name: 'Gosling',
username: 'gosling',
language_code: 'en',
is_premium: true,
allows_write_to_pm: true,
}),
],
[
'hash',
'89d6079ad6762351f38c6dbbc41bb53048019256a9443988af7a48bcad16ba31',
],
['auth_date', '1716922846'],
['start_param', 'debug'],
['chat_type', 'sender'],
['chat_instance', '8428209589180549439'],
]).toString()
}

/**
* Applies the mocked Telegram environment.
*
* @param {string} initDataRaw - initData in URLSearchParams format
*/
function applyMockEnvironment(initDataRaw: string): void {
mockTelegramEnv({
themeParams: {
accentTextColor: '#6ab2f2',
bgColor: '#17212b',
buttonColor: '#5288c1',
buttonTextColor: '#ffffff',
destructiveTextColor: '#ec3942',
headerBgColor: '#17212b',
hintColor: '#708499',
linkColor: '#6ab3f3',
secondaryBgColor: '#232e3c',
sectionBgColor: '#17212b',
sectionHeaderTextColor: '#6ab3f3',
subtitleTextColor: '#708499',
textColor: '#f5f5f5',
},
initData: parseInitData(initDataRaw),
initDataRaw,
version: '8',
platform: 'tdesktop',
})

sessionStorage.setItem('____mocked', '1')
}

/**
* Logs a warning message about the mocking to the console.
*/
function logMockWarning(): void {
console.info(
'⚠️ As the current environment was not considered Telegram-based, it has been mocked. ' +
'Please note that you should not do this in production, and this behavior is specific ' +
'to the development process only. Environment mocking is applied only in development mode. ' +
'Therefore, after building the application, you will not see this behavior or the related ' +
'warning, which would lead to the application crashing outside of Telegram.',
)
}
Loading