nuxt-telegram-mini-app 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -0
- package/.vscode/settings.json +55 -0
- package/.vscode/tailwind.json +55 -0
- package/CONTRIBUTING.md +406 -0
- package/LICENSE +21 -0
- package/README.md +640 -0
- package/app/app.vue +87 -0
- package/app/assets/css/main.css +53 -0
- package/app/assets/css/tailwind.css +3 -0
- package/app/components/ErrorBoundary.vue +81 -0
- package/app/components/Hero.vue +61 -0
- package/app/components/tg/Button.vue +128 -0
- package/app/components/tg/Cell.vue +91 -0
- package/app/components/tg/Content.vue +42 -0
- package/app/components/tg/Nav.vue +107 -0
- package/app/components/tg/Section.vue +50 -0
- package/app/composables/telegram.ts +342 -0
- package/app/error.vue +161 -0
- package/app/pages/components.vue +279 -0
- package/app/pages/functions.vue +107 -0
- package/app/pages/index.vue +211 -0
- package/app/pages/utilities.vue +402 -0
- package/app/types/telegram-webapp.ts +160 -0
- package/app/utils/color.ts +37 -0
- package/eslint.config.mjs +6 -0
- package/nuxt.config.ts +55 -0
- package/package.json +46 -0
- package/public/_redirects +2 -0
- package/public/favicon.ico +0 -0
- package/public/img/hero-user.svg +8 -0
- package/public/img/nuxt-logo.svg +11 -0
- package/public/robots.txt +2 -0
- package/server/api/verify-telegram-data.post.ts +150 -0
- package/tailwind.config.ts +39 -0
- package/tests/components.spec.ts +311 -0
- package/tests/pages.spec.ts +426 -0
- package/tests/telegram.spec.ts +105 -0
- package/tests/utils.spec.ts +47 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +24 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section :class="sectionClass">
|
|
3
|
+
<header v-if="title" :class="inset ? 'px-4' : 'last:pb-0'">
|
|
4
|
+
<h2 class="px-3 text-sm uppercase mb-1.5" :style="{ color: 'var(--tg-theme-section-header-text-color)' }">{{ title }}</h2>
|
|
5
|
+
</header>
|
|
6
|
+
<div class="bg-bg" :class="bodyClass" :style="{ backgroundColor: 'var(--tg-theme-section-bg-color)' }">
|
|
7
|
+
<slot />
|
|
8
|
+
</div>
|
|
9
|
+
<footer v-if="$slots.append" class="pt-2 px-5 text-xs text-hint" :class="appendBorder ? 'border-t border-sectionSeparator' : ''">
|
|
10
|
+
<slot name="append" />
|
|
11
|
+
</footer>
|
|
12
|
+
</section>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script setup lang="ts">
|
|
16
|
+
import { computed } from 'vue'
|
|
17
|
+
|
|
18
|
+
const props = withDefaults(defineProps<{
|
|
19
|
+
title?: string
|
|
20
|
+
/** Adds outer rounding/indents reminiscent of iOS */
|
|
21
|
+
inset?: boolean
|
|
22
|
+
/** Adds a thin top border above the append slot */
|
|
23
|
+
appendBorder?: boolean
|
|
24
|
+
/** Inner background tone */
|
|
25
|
+
tone?: 'default' | 'secondary'
|
|
26
|
+
/** Inner padding of the body (removed: default only) */
|
|
27
|
+
class?: string
|
|
28
|
+
}>(), {
|
|
29
|
+
title: undefined,
|
|
30
|
+
inset: false,
|
|
31
|
+
appendBorder: true,
|
|
32
|
+
tone: 'default',
|
|
33
|
+
class: '',
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const sectionClass = computed(() => [props.inset ? 'px-3' : '', props.class].filter(Boolean).join(' '))
|
|
37
|
+
|
|
38
|
+
const roundedClass = computed(() => props.inset ? 'rounded-2xl overflow-hidden' : 'rounded')
|
|
39
|
+
|
|
40
|
+
const paddingClass = computed(() => '')
|
|
41
|
+
|
|
42
|
+
const toneClass = computed(() => props.tone === 'secondary' ? 'bg-secondaryBg' : 'bg-bg')
|
|
43
|
+
|
|
44
|
+
const borderClass = computed(() => '')
|
|
45
|
+
|
|
46
|
+
const bodyClass = computed(() => [roundedClass.value, borderClass.value, toneClass.value, paddingClass.value].filter(Boolean).join(' '))
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<style scoped>
|
|
50
|
+
</style>
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { ref, computed, onMounted } from 'vue'
|
|
2
|
+
import type {
|
|
3
|
+
TelegramWebApp,
|
|
4
|
+
TelegramWebAppInitData
|
|
5
|
+
} from '~/types/telegram-webapp'
|
|
6
|
+
|
|
7
|
+
// Helper to get WebApp instance
|
|
8
|
+
function getWebApp(): TelegramWebApp | null {
|
|
9
|
+
if (typeof window === 'undefined') return null
|
|
10
|
+
return window.Telegram?.WebApp || null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Shared WebApp instance
|
|
14
|
+
let globalWebApp: TelegramWebApp | null = null
|
|
15
|
+
let isInitialized = false
|
|
16
|
+
|
|
17
|
+
// Initialize WebApp once
|
|
18
|
+
function initWebApp() {
|
|
19
|
+
if (isInitialized) return
|
|
20
|
+
if (typeof window === 'undefined') return
|
|
21
|
+
|
|
22
|
+
globalWebApp = getWebApp()
|
|
23
|
+
if (globalWebApp) {
|
|
24
|
+
globalWebApp.ready()
|
|
25
|
+
isInitialized = true
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Main composable to access Telegram WebApp
|
|
30
|
+
export function useTelegramWebApp() {
|
|
31
|
+
const webApp = ref<TelegramWebApp | null>(null)
|
|
32
|
+
const isReady = ref(false)
|
|
33
|
+
|
|
34
|
+
onMounted(() => {
|
|
35
|
+
initWebApp()
|
|
36
|
+
webApp.value = globalWebApp
|
|
37
|
+
isReady.value = isInitialized
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
webApp: computed(() => webApp.value),
|
|
42
|
+
isReady: computed(() => isReady.value),
|
|
43
|
+
isAvailable: computed(() => !!webApp.value)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Back Button
|
|
48
|
+
export function useBackButton() {
|
|
49
|
+
const visible = ref(false)
|
|
50
|
+
|
|
51
|
+
onMounted(() => {
|
|
52
|
+
initWebApp()
|
|
53
|
+
if (globalWebApp?.BackButton) {
|
|
54
|
+
visible.value = globalWebApp.BackButton.isVisible
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
supported: computed(() => !!globalWebApp?.BackButton),
|
|
60
|
+
mounted: computed(() => !!globalWebApp?.BackButton),
|
|
61
|
+
visible: computed(() => visible.value),
|
|
62
|
+
mount: () => true, // Always available with official SDK
|
|
63
|
+
unmount: () => true,
|
|
64
|
+
show: () => {
|
|
65
|
+
if (globalWebApp?.BackButton) {
|
|
66
|
+
globalWebApp.BackButton.show()
|
|
67
|
+
visible.value = true
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
hide: () => {
|
|
71
|
+
if (globalWebApp?.BackButton) {
|
|
72
|
+
globalWebApp.BackButton.hide()
|
|
73
|
+
visible.value = false
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
onClick: (fn: () => void) => {
|
|
77
|
+
if (globalWebApp?.BackButton) {
|
|
78
|
+
globalWebApp.BackButton.onClick(fn)
|
|
79
|
+
return fn
|
|
80
|
+
}
|
|
81
|
+
return () => {}
|
|
82
|
+
},
|
|
83
|
+
offClick: (fn: () => void) => {
|
|
84
|
+
if (globalWebApp?.BackButton) {
|
|
85
|
+
globalWebApp.BackButton.offClick(fn)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Haptic Feedback
|
|
92
|
+
export function useHapticFeedback() {
|
|
93
|
+
onMounted(() => {
|
|
94
|
+
initWebApp()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
supported: computed(() => !!globalWebApp?.HapticFeedback),
|
|
99
|
+
impactOccurred: (style: 'light' | 'medium' | 'heavy' = 'medium') => {
|
|
100
|
+
if (globalWebApp?.HapticFeedback) {
|
|
101
|
+
globalWebApp.HapticFeedback.impactOccurred(style)
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
notificationOccurred: (type: 'error' | 'success' | 'warning') => {
|
|
105
|
+
if (globalWebApp?.HapticFeedback) {
|
|
106
|
+
globalWebApp.HapticFeedback.notificationOccurred(type)
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
selectionChanged: () => {
|
|
110
|
+
if (globalWebApp?.HapticFeedback) {
|
|
111
|
+
globalWebApp.HapticFeedback.selectionChanged()
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Init Data
|
|
118
|
+
export function useInitData() {
|
|
119
|
+
onMounted(() => {
|
|
120
|
+
initWebApp()
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const state = computed<TelegramWebAppInitData | undefined>(() =>
|
|
124
|
+
globalWebApp?.initDataUnsafe || undefined
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
const raw = computed<string | undefined>(() =>
|
|
128
|
+
globalWebApp?.initData || undefined
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
state,
|
|
133
|
+
raw,
|
|
134
|
+
user: computed(() => state.value?.user),
|
|
135
|
+
receiver: computed(() => state.value?.receiver),
|
|
136
|
+
chat: computed(() => state.value?.chat),
|
|
137
|
+
authDate: computed(() => state.value?.auth_date),
|
|
138
|
+
queryId: computed(() => state.value?.query_id),
|
|
139
|
+
startParam: computed(() => state.value?.start_param),
|
|
140
|
+
restore: () => {
|
|
141
|
+
// Official SDK handles this automatically
|
|
142
|
+
return true
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Main Button
|
|
148
|
+
export function useMainButton() {
|
|
149
|
+
onMounted(() => {
|
|
150
|
+
initWebApp()
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
mounted: computed(() => !!globalWebApp?.MainButton),
|
|
155
|
+
enabled: computed(() => globalWebApp?.MainButton?.isActive || false),
|
|
156
|
+
loaderVisible: computed(() => globalWebApp?.MainButton?.isProgressVisible || false),
|
|
157
|
+
visible: computed(() => globalWebApp?.MainButton?.isVisible || false),
|
|
158
|
+
state: computed(() => ({
|
|
159
|
+
text: globalWebApp?.MainButton?.text || '',
|
|
160
|
+
color: globalWebApp?.MainButton?.color || '',
|
|
161
|
+
text_color: globalWebApp?.MainButton?.textColor || '',
|
|
162
|
+
is_active: globalWebApp?.MainButton?.isActive || false,
|
|
163
|
+
is_visible: globalWebApp?.MainButton?.isVisible || false
|
|
164
|
+
})),
|
|
165
|
+
text: computed(() => globalWebApp?.MainButton?.text || ''),
|
|
166
|
+
textColor: computed(() => globalWebApp?.MainButton?.textColor),
|
|
167
|
+
backgroundColor: computed(() => globalWebApp?.MainButton?.color),
|
|
168
|
+
mount: () => true, // Always available
|
|
169
|
+
unmount: () => true,
|
|
170
|
+
onClick: (fn: () => void) => {
|
|
171
|
+
if (globalWebApp?.MainButton) {
|
|
172
|
+
globalWebApp.MainButton.onClick(fn)
|
|
173
|
+
return fn
|
|
174
|
+
}
|
|
175
|
+
return () => {}
|
|
176
|
+
},
|
|
177
|
+
offClick: (fn: () => void) => {
|
|
178
|
+
if (globalWebApp?.MainButton) {
|
|
179
|
+
globalWebApp.MainButton.offClick(fn)
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
setParams: (updates: {
|
|
183
|
+
text?: string
|
|
184
|
+
color?: string
|
|
185
|
+
text_color?: string
|
|
186
|
+
is_active?: boolean
|
|
187
|
+
is_visible?: boolean
|
|
188
|
+
}) => {
|
|
189
|
+
if (globalWebApp?.MainButton) {
|
|
190
|
+
globalWebApp.MainButton.setParams(updates)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Mini App / Theme management
|
|
197
|
+
export function useMiniApp() {
|
|
198
|
+
onMounted(() => {
|
|
199
|
+
initWebApp()
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
supported: computed(() => !!globalWebApp),
|
|
204
|
+
mounted: computed(() => !!globalWebApp),
|
|
205
|
+
active: computed(() => true), // Always active when available
|
|
206
|
+
dark: computed(() => globalWebApp?.colorScheme === 'dark'),
|
|
207
|
+
state: computed(() => globalWebApp || null),
|
|
208
|
+
backgroundColor: computed(() => globalWebApp?.backgroundColor),
|
|
209
|
+
headerColor: computed(() => globalWebApp?.headerColor),
|
|
210
|
+
bottomBarColor: computed(() => globalWebApp?.bottomBarColor),
|
|
211
|
+
setBackgroundColor: (color: string) => {
|
|
212
|
+
if (globalWebApp) {
|
|
213
|
+
globalWebApp.setBackgroundColor(color)
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
setHeaderColor: (color: string) => {
|
|
217
|
+
if (globalWebApp) {
|
|
218
|
+
globalWebApp.setHeaderColor(color)
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
setBottomBarColor: (color: string) => {
|
|
222
|
+
if (globalWebApp) {
|
|
223
|
+
globalWebApp.setBottomBarColor(color)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Theme Params
|
|
230
|
+
export function useThemeParams() {
|
|
231
|
+
onMounted(() => {
|
|
232
|
+
initWebApp()
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
const themeParams = computed(() => globalWebApp?.themeParams || {})
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
mounted: computed(() => !!globalWebApp),
|
|
239
|
+
dark: computed(() => globalWebApp?.colorScheme === 'dark'),
|
|
240
|
+
state: computed(() => themeParams.value),
|
|
241
|
+
backgroundColor: computed(() => themeParams.value.bg_color),
|
|
242
|
+
textColor: computed(() => themeParams.value.text_color),
|
|
243
|
+
accentTextColor: computed(() => themeParams.value.accent_text_color),
|
|
244
|
+
hintColor: computed(() => themeParams.value.hint_color),
|
|
245
|
+
buttonColor: computed(() => themeParams.value.button_color),
|
|
246
|
+
buttonTextColor: computed(() => themeParams.value.button_text_color),
|
|
247
|
+
destructiveTextColor: computed(() => themeParams.value.destructive_text_color),
|
|
248
|
+
linkColor: computed(() => themeParams.value.link_color),
|
|
249
|
+
secondaryBackgroundColor: computed(() => themeParams.value.secondary_bg_color),
|
|
250
|
+
sectionBackgroundColor: computed(() => themeParams.value.section_bg_color),
|
|
251
|
+
headerBackgroundColor: computed(() => themeParams.value.header_bg_color),
|
|
252
|
+
subtitleTextColor: computed(() => themeParams.value.subtitle_text_color),
|
|
253
|
+
sectionHeaderTextColor: computed(() => themeParams.value.section_header_text_color),
|
|
254
|
+
sectionSeparatorColor: computed(() => themeParams.value.section_separator_color)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Viewport
|
|
259
|
+
export function useViewport() {
|
|
260
|
+
onMounted(() => {
|
|
261
|
+
initWebApp()
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
mounted: computed(() => !!globalWebApp),
|
|
266
|
+
width: computed(() => {
|
|
267
|
+
// Telegram WebApp doesn't expose viewport width directly, use window.innerWidth
|
|
268
|
+
return typeof window !== 'undefined' ? window.innerWidth : 0
|
|
269
|
+
}),
|
|
270
|
+
height: computed(() => globalWebApp?.viewportHeight || 0),
|
|
271
|
+
stableHeight: computed(() => globalWebApp?.viewportStableHeight || 0),
|
|
272
|
+
expanded: computed(() => globalWebApp?.isExpanded || false),
|
|
273
|
+
stable: computed(() => {
|
|
274
|
+
// Consider stable if viewport height equals stable height
|
|
275
|
+
const vh = globalWebApp?.viewportHeight || 0
|
|
276
|
+
const svh = globalWebApp?.viewportStableHeight || 0
|
|
277
|
+
return vh === svh
|
|
278
|
+
}),
|
|
279
|
+
insets: computed(() => ({
|
|
280
|
+
top: 0,
|
|
281
|
+
bottom: 0,
|
|
282
|
+
left: 0,
|
|
283
|
+
right: 0
|
|
284
|
+
})),
|
|
285
|
+
requestFullscreen: () => {
|
|
286
|
+
// Not available in Telegram WebApp
|
|
287
|
+
},
|
|
288
|
+
exitFullscreen: () => {
|
|
289
|
+
// Not available in Telegram WebApp
|
|
290
|
+
},
|
|
291
|
+
expand: () => {
|
|
292
|
+
if (globalWebApp) {
|
|
293
|
+
globalWebApp.expand()
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Links and sharing
|
|
300
|
+
export function openLink(url: string, options?: { try_instant_view?: boolean }) {
|
|
301
|
+
const webApp = getWebApp()
|
|
302
|
+
if (webApp) {
|
|
303
|
+
webApp.openLink(url, options)
|
|
304
|
+
} else {
|
|
305
|
+
// Fallback to regular window.open
|
|
306
|
+
window.open(url, '_blank')
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function openTelegramLink(url: string) {
|
|
311
|
+
const webApp = getWebApp()
|
|
312
|
+
if (webApp) {
|
|
313
|
+
webApp.openTelegramLink(url)
|
|
314
|
+
} else {
|
|
315
|
+
// Fallback to regular window.open
|
|
316
|
+
window.open(url, '_blank')
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function shareURL(url: string) {
|
|
321
|
+
const webApp = getWebApp()
|
|
322
|
+
if (webApp) {
|
|
323
|
+
// Use Web Share API if available, otherwise copy to clipboard
|
|
324
|
+
if (navigator.share) {
|
|
325
|
+
navigator.share({ url }).catch(() => {
|
|
326
|
+
// Fallback to clipboard if share fails
|
|
327
|
+
if (navigator.clipboard) {
|
|
328
|
+
navigator.clipboard.writeText(url)
|
|
329
|
+
}
|
|
330
|
+
})
|
|
331
|
+
} else if (navigator.clipboard) {
|
|
332
|
+
navigator.clipboard.writeText(url)
|
|
333
|
+
}
|
|
334
|
+
} else {
|
|
335
|
+
// Fallback to navigator.share or clipboard
|
|
336
|
+
if (navigator.share) {
|
|
337
|
+
navigator.share({ url })
|
|
338
|
+
} else if (navigator.clipboard) {
|
|
339
|
+
navigator.clipboard.writeText(url)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
package/app/error.vue
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="min-h-screen tg-bg flex items-center justify-center p-4">
|
|
3
|
+
<div class="max-w-md w-full text-center space-y-6">
|
|
4
|
+
<div class="space-y-2">
|
|
5
|
+
<h1 class="text-2xl font-bold text-tg-text">
|
|
6
|
+
{{ error?.statusCode === 404 ? 'Page Not Found' : 'Something went wrong' }}
|
|
7
|
+
</h1>
|
|
8
|
+
<p class="text-tg-hint">
|
|
9
|
+
{{ error?.statusCode === 404
|
|
10
|
+
? 'The page you\'re looking for doesn\'t exist.'
|
|
11
|
+
: 'We encountered an unexpected error. Please try again.'
|
|
12
|
+
}}
|
|
13
|
+
</p>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div class="space-y-3">
|
|
17
|
+
<button
|
|
18
|
+
@click="handleError"
|
|
19
|
+
class="w-full bg-tg-button text-tg-button-text py-3 px-4 rounded-lg font-medium hover:opacity-90 transition-opacity"
|
|
20
|
+
>
|
|
21
|
+
{{ error?.statusCode === 404 ? 'Go Home' : 'Try Again' }}
|
|
22
|
+
</button>
|
|
23
|
+
|
|
24
|
+
<button
|
|
25
|
+
@click="clearAndReload"
|
|
26
|
+
class="w-full border border-tg-button text-tg-button py-3 px-4 rounded-lg font-medium hover:bg-tg-button hover:text-tg-button-text transition-colors"
|
|
27
|
+
>
|
|
28
|
+
Clear Cache & Reload
|
|
29
|
+
</button>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<details class="text-left text-sm text-tg-hint">
|
|
33
|
+
<summary class="cursor-pointer hover:text-tg-text">Technical Details</summary>
|
|
34
|
+
<div class="mt-2 p-3 bg-tg-secondary-bg rounded border text-xs font-mono">
|
|
35
|
+
<div><strong>Status:</strong> {{ error?.statusCode || 'Unknown' }}</div>
|
|
36
|
+
<div><strong>Message:</strong> {{ error?.statusMessage || 'No details available' }}</div>
|
|
37
|
+
<div><strong>URL:</strong> {{ error?.url || 'Unknown' }}</div>
|
|
38
|
+
<div v-if="telegramInfo"><strong>Telegram:</strong> {{ telegramInfo }}</div>
|
|
39
|
+
</div>
|
|
40
|
+
</details>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<script setup lang="ts">
|
|
46
|
+
import { computed } from 'vue'
|
|
47
|
+
|
|
48
|
+
// Define props for the error
|
|
49
|
+
const props = defineProps<{
|
|
50
|
+
error: {
|
|
51
|
+
statusCode?: number
|
|
52
|
+
statusMessage?: string
|
|
53
|
+
message?: string
|
|
54
|
+
url?: string
|
|
55
|
+
}
|
|
56
|
+
}>()
|
|
57
|
+
|
|
58
|
+
// Get Telegram context info for debugging
|
|
59
|
+
const telegramInfo = computed(() => {
|
|
60
|
+
if (typeof window === 'undefined') return null
|
|
61
|
+
|
|
62
|
+
const info: string[] = []
|
|
63
|
+
|
|
64
|
+
// Check if running in Telegram
|
|
65
|
+
const tg = (window as any)?.Telegram?.WebApp
|
|
66
|
+
if (tg) {
|
|
67
|
+
info.push('In Telegram WebApp')
|
|
68
|
+
info.push(`Platform: ${tg.platform || 'unknown'}`)
|
|
69
|
+
info.push(`Version: ${tg.version || 'unknown'}`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check for hash parameters
|
|
73
|
+
const hash = window.location.hash
|
|
74
|
+
if (hash && hash.includes('tgWebApp')) {
|
|
75
|
+
info.push('Has TG hash params')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check for session storage flags
|
|
79
|
+
if (sessionStorage.getItem('__tg_hash_fixed')) {
|
|
80
|
+
info.push('Hash was fixed')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return info.length > 0 ? info.join(', ') : 'Not in Telegram'
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
function handleError() {
|
|
87
|
+
if (props.error?.statusCode === 404) {
|
|
88
|
+
// Navigate to home
|
|
89
|
+
navigateTo('/')
|
|
90
|
+
} else {
|
|
91
|
+
// Try to reload the current page
|
|
92
|
+
if (typeof window !== 'undefined') {
|
|
93
|
+
window.location.reload()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function clearAndReload() {
|
|
99
|
+
if (typeof window !== 'undefined') {
|
|
100
|
+
// Clear all storage
|
|
101
|
+
try {
|
|
102
|
+
sessionStorage.clear()
|
|
103
|
+
localStorage.clear()
|
|
104
|
+
} catch (e) {
|
|
105
|
+
console.warn('Failed to clear storage:', e)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Clear hash fix flag specifically
|
|
109
|
+
try {
|
|
110
|
+
sessionStorage.removeItem('__tg_hash_fixed')
|
|
111
|
+
} catch (e) {
|
|
112
|
+
console.warn('Failed to clear hash fix flag:', e)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Reload without hash if there are problematic parameters
|
|
116
|
+
const hash = window.location.hash
|
|
117
|
+
if (hash && hash.includes('tgWebApp')) {
|
|
118
|
+
// Try to reload without the problematic hash
|
|
119
|
+
window.location.href = window.location.pathname + window.location.search
|
|
120
|
+
} else {
|
|
121
|
+
window.location.reload()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Set page title
|
|
127
|
+
useHead({
|
|
128
|
+
title: computed(() =>
|
|
129
|
+
props.error?.statusCode === 404
|
|
130
|
+
? 'Page Not Found'
|
|
131
|
+
: 'Error - Telegram Mini App'
|
|
132
|
+
)
|
|
133
|
+
})
|
|
134
|
+
</script>
|
|
135
|
+
|
|
136
|
+
<style scoped>
|
|
137
|
+
/* Use Telegram theme colors */
|
|
138
|
+
.tg-bg {
|
|
139
|
+
background-color: var(--tg-theme-bg-color, #ffffff);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.tg-text {
|
|
143
|
+
color: var(--tg-theme-text-color, #000000);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.tg-hint {
|
|
147
|
+
color: var(--tg-theme-hint-color, #999999);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.tg-button {
|
|
151
|
+
background-color: var(--tg-theme-button-color, #2481cc);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.tg-button-text {
|
|
155
|
+
color: var(--tg-theme-button-text-color, #ffffff);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.tg-secondary-bg {
|
|
159
|
+
background-color: var(--tg-theme-secondary-bg-color, #f1f1f1);
|
|
160
|
+
}
|
|
161
|
+
</style>
|