nuxt-telegram-mini-app 0.0.1 → 0.0.3

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.
@@ -1,7 +1,15 @@
1
1
  import { ref, computed, onMounted } from 'vue'
2
2
  import type {
3
3
  TelegramWebApp,
4
- TelegramWebAppInitData
4
+ TelegramDownloadFileParams,
5
+ TelegramEmojiStatusParams,
6
+ TelegramHomeScreenStatus,
7
+ TelegramLocationData,
8
+ TelegramStoryShareParams,
9
+ TelegramWebAppDeviceStorage,
10
+ TelegramWebAppInitData,
11
+ TelegramWebAppSecureStorage,
12
+ TelegramWebAppInsets
5
13
  } from '~/types/telegram-webapp'
6
14
 
7
15
  // Helper to get WebApp instance
@@ -26,6 +34,29 @@ function initWebApp() {
26
34
  }
27
35
  }
28
36
 
37
+ function ensureWebApp() {
38
+ initWebApp()
39
+ return globalWebApp || getWebApp()
40
+ }
41
+
42
+ const emptyInsets: TelegramWebAppInsets = {
43
+ top: 0,
44
+ bottom: 0,
45
+ left: 0,
46
+ right: 0
47
+ }
48
+
49
+ function storageCallback<T>(resolve: (value: T) => void, reject: (reason?: unknown) => void) {
50
+ return (error: unknown, result?: T) => {
51
+ if (error) {
52
+ reject(error)
53
+ return
54
+ }
55
+
56
+ resolve(result as T)
57
+ }
58
+ }
59
+
29
60
  // Main composable to access Telegram WebApp
30
61
  export function useTelegramWebApp() {
31
62
  const webApp = ref<TelegramWebApp | null>(null)
@@ -47,31 +78,39 @@ export function useTelegramWebApp() {
47
78
  // Back Button
48
79
  export function useBackButton() {
49
80
  const visible = ref(false)
81
+ const mounted = ref(false)
50
82
 
51
83
  onMounted(() => {
52
84
  initWebApp()
53
85
  if (globalWebApp?.BackButton) {
54
- visible.value = globalWebApp.BackButton.isVisible
86
+ visible.value = globalWebApp.BackButton.isVisible ?? false
55
87
  }
56
88
  })
57
89
 
58
90
  return {
59
91
  supported: computed(() => !!globalWebApp?.BackButton),
60
- mounted: computed(() => !!globalWebApp?.BackButton),
92
+ mounted: computed(() => mounted.value),
61
93
  visible: computed(() => visible.value),
62
- mount: () => true, // Always available with official SDK
63
- unmount: () => true,
94
+ mount: () => {
95
+ mounted.value = true
96
+ return true
97
+ },
98
+ unmount: () => {
99
+ mounted.value = false
100
+ visible.value = false
101
+ return true
102
+ },
64
103
  show: () => {
65
- if (globalWebApp?.BackButton) {
104
+ if (globalWebApp?.BackButton?.show) {
66
105
  globalWebApp.BackButton.show()
67
- visible.value = true
68
106
  }
107
+ visible.value = true
69
108
  },
70
109
  hide: () => {
71
- if (globalWebApp?.BackButton) {
110
+ if (globalWebApp?.BackButton?.hide) {
72
111
  globalWebApp.BackButton.hide()
73
- visible.value = false
74
112
  }
113
+ visible.value = false
75
114
  },
76
115
  onClick: (fn: () => void) => {
77
116
  if (globalWebApp?.BackButton) {
@@ -96,7 +135,7 @@ export function useHapticFeedback() {
96
135
 
97
136
  return {
98
137
  supported: computed(() => !!globalWebApp?.HapticFeedback),
99
- impactOccurred: (style: 'light' | 'medium' | 'heavy' = 'medium') => {
138
+ impactOccurred: (style: 'light' | 'medium' | 'heavy' | 'rigid' | 'soft' = 'medium') => {
100
139
  if (globalWebApp?.HapticFeedback) {
101
140
  globalWebApp.HapticFeedback.impactOccurred(style)
102
141
  }
@@ -137,6 +176,7 @@ export function useInitData() {
137
176
  authDate: computed(() => state.value?.auth_date),
138
177
  queryId: computed(() => state.value?.query_id),
139
178
  startParam: computed(() => state.value?.start_param),
179
+ signature: computed(() => state.value?.signature),
140
180
  restore: () => {
141
181
  // Official SDK handles this automatically
142
182
  return true
@@ -146,27 +186,50 @@ export function useInitData() {
146
186
 
147
187
  // Main Button
148
188
  export function useMainButton() {
189
+ const mounted = ref(false)
190
+ const state = ref({
191
+ icon_custom_emoji_id: '',
192
+ text: '',
193
+ color: '',
194
+ text_color: '',
195
+ has_shine_effect: false,
196
+ is_active: false,
197
+ is_visible: false
198
+ })
199
+
149
200
  onMounted(() => {
150
201
  initWebApp()
202
+ if (globalWebApp?.MainButton) {
203
+ state.value = {
204
+ icon_custom_emoji_id: globalWebApp.MainButton.iconCustomEmojiId || '',
205
+ text: globalWebApp.MainButton.text || '',
206
+ color: globalWebApp.MainButton.color || '',
207
+ text_color: globalWebApp.MainButton.textColor || '',
208
+ has_shine_effect: globalWebApp.MainButton.hasShineEffect || false,
209
+ is_active: globalWebApp.MainButton.isActive || false,
210
+ is_visible: globalWebApp.MainButton.isVisible || false
211
+ }
212
+ }
151
213
  })
152
214
 
153
215
  return {
154
- mounted: computed(() => !!globalWebApp?.MainButton),
155
- enabled: computed(() => globalWebApp?.MainButton?.isActive || false),
216
+ mounted: computed(() => mounted.value),
217
+ enabled: computed(() => state.value.is_active),
156
218
  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,
219
+ visible: computed(() => state.value.is_visible),
220
+ state: computed(() => state.value),
221
+ text: computed(() => state.value.text),
222
+ textColor: computed(() => state.value.text_color || undefined),
223
+ backgroundColor: computed(() => state.value.color || undefined),
224
+ mount: () => {
225
+ mounted.value = true
226
+ return true
227
+ },
228
+ unmount: () => {
229
+ mounted.value = false
230
+ state.value.is_visible = false
231
+ return true
232
+ },
170
233
  onClick: (fn: () => void) => {
171
234
  if (globalWebApp?.MainButton) {
172
235
  globalWebApp.MainButton.onClick(fn)
@@ -180,19 +243,88 @@ export function useMainButton() {
180
243
  }
181
244
  },
182
245
  setParams: (updates: {
246
+ icon_custom_emoji_id?: string
183
247
  text?: string
184
248
  color?: string
185
249
  text_color?: string
250
+ has_shine_effect?: boolean
251
+ position?: 'left' | 'right' | 'top' | 'bottom'
186
252
  is_active?: boolean
187
253
  is_visible?: boolean
188
254
  }) => {
189
- if (globalWebApp?.MainButton) {
255
+ state.value = {
256
+ ...state.value,
257
+ ...updates
258
+ }
259
+ if (globalWebApp?.MainButton?.setParams) {
190
260
  globalWebApp.MainButton.setParams(updates)
191
261
  }
192
262
  }
193
263
  }
194
264
  }
195
265
 
266
+ export function useSecondaryButton() {
267
+ onMounted(() => {
268
+ initWebApp()
269
+ })
270
+
271
+ return {
272
+ mounted: computed(() => !!globalWebApp?.SecondaryButton),
273
+ enabled: computed(() => globalWebApp?.SecondaryButton?.isActive || false),
274
+ loaderVisible: computed(() => globalWebApp?.SecondaryButton?.isProgressVisible || false),
275
+ visible: computed(() => globalWebApp?.SecondaryButton?.isVisible || false),
276
+ text: computed(() => globalWebApp?.SecondaryButton?.text || ''),
277
+ textColor: computed(() => globalWebApp?.SecondaryButton?.textColor),
278
+ backgroundColor: computed(() => globalWebApp?.SecondaryButton?.color),
279
+ position: computed(() => globalWebApp?.SecondaryButton?.position),
280
+ onClick: (fn: () => void) => {
281
+ if (globalWebApp?.SecondaryButton) {
282
+ globalWebApp.SecondaryButton.onClick(fn)
283
+ return fn
284
+ }
285
+ return () => {}
286
+ },
287
+ offClick: (fn: () => void) => {
288
+ globalWebApp?.SecondaryButton?.offClick(fn)
289
+ },
290
+ setParams: (updates: {
291
+ icon_custom_emoji_id?: string
292
+ text?: string
293
+ color?: string
294
+ text_color?: string
295
+ has_shine_effect?: boolean
296
+ position?: 'left' | 'right' | 'top' | 'bottom'
297
+ is_active?: boolean
298
+ is_visible?: boolean
299
+ }) => {
300
+ globalWebApp?.SecondaryButton?.setParams(updates)
301
+ }
302
+ }
303
+ }
304
+
305
+ export function useSettingsButton() {
306
+ onMounted(() => {
307
+ initWebApp()
308
+ })
309
+
310
+ return {
311
+ supported: computed(() => !!globalWebApp?.SettingsButton),
312
+ visible: computed(() => globalWebApp?.SettingsButton?.isVisible || false),
313
+ show: () => globalWebApp?.SettingsButton?.show(),
314
+ hide: () => globalWebApp?.SettingsButton?.hide(),
315
+ onClick: (fn: () => void) => {
316
+ if (globalWebApp?.SettingsButton) {
317
+ globalWebApp.SettingsButton.onClick(fn)
318
+ return fn
319
+ }
320
+ return () => {}
321
+ },
322
+ offClick: (fn: () => void) => {
323
+ globalWebApp?.SettingsButton?.offClick(fn)
324
+ }
325
+ }
326
+ }
327
+
196
328
  // Mini App / Theme management
197
329
  export function useMiniApp() {
198
330
  onMounted(() => {
@@ -203,6 +335,9 @@ export function useMiniApp() {
203
335
  supported: computed(() => !!globalWebApp),
204
336
  mounted: computed(() => !!globalWebApp),
205
337
  active: computed(() => true), // Always active when available
338
+ foreground: computed(() => globalWebApp?.isActive ?? true),
339
+ fullscreen: computed(() => globalWebApp?.isFullscreen || false),
340
+ orientationLocked: computed(() => globalWebApp?.isOrientationLocked || false),
206
341
  dark: computed(() => globalWebApp?.colorScheme === 'dark'),
207
342
  state: computed(() => globalWebApp || null),
208
343
  backgroundColor: computed(() => globalWebApp?.backgroundColor),
@@ -222,7 +357,14 @@ export function useMiniApp() {
222
357
  if (globalWebApp) {
223
358
  globalWebApp.setBottomBarColor(color)
224
359
  }
225
- }
360
+ },
361
+ enableClosingConfirmation: () => globalWebApp?.enableClosingConfirmation(),
362
+ disableClosingConfirmation: () => globalWebApp?.disableClosingConfirmation(),
363
+ enableVerticalSwipes: () => globalWebApp?.enableVerticalSwipes(),
364
+ disableVerticalSwipes: () => globalWebApp?.disableVerticalSwipes(),
365
+ lockOrientation: () => globalWebApp?.lockOrientation?.(),
366
+ unlockOrientation: () => globalWebApp?.unlockOrientation?.(),
367
+ hideKeyboard: () => globalWebApp?.hideKeyboard?.()
226
368
  }
227
369
  }
228
370
 
@@ -277,16 +419,17 @@ export function useViewport() {
277
419
  return vh === svh
278
420
  }),
279
421
  insets: computed(() => ({
280
- top: 0,
281
- bottom: 0,
282
- left: 0,
283
- right: 0
422
+ ...(globalWebApp?.safeAreaInset || emptyInsets)
423
+ })),
424
+ contentInsets: computed(() => ({
425
+ ...(globalWebApp?.contentSafeAreaInset || emptyInsets)
284
426
  })),
427
+ fullscreen: computed(() => globalWebApp?.isFullscreen || false),
285
428
  requestFullscreen: () => {
286
- // Not available in Telegram WebApp
429
+ globalWebApp?.requestFullscreen?.()
287
430
  },
288
431
  exitFullscreen: () => {
289
- // Not available in Telegram WebApp
432
+ globalWebApp?.exitFullscreen?.()
290
433
  },
291
434
  expand: () => {
292
435
  if (globalWebApp) {
@@ -296,9 +439,155 @@ export function useViewport() {
296
439
  }
297
440
  }
298
441
 
442
+ export function useHomeScreen() {
443
+ onMounted(() => {
444
+ initWebApp()
445
+ })
446
+
447
+ const status = ref<TelegramHomeScreenStatus | undefined>()
448
+
449
+ return {
450
+ supported: computed(() => !!globalWebApp?.addToHomeScreen || !!globalWebApp?.checkHomeScreenStatus),
451
+ status: computed(() => status.value),
452
+ add: () => globalWebApp?.addToHomeScreen?.(),
453
+ check: () => {
454
+ globalWebApp?.checkHomeScreenStatus?.((nextStatus) => {
455
+ status.value = nextStatus
456
+ })
457
+ }
458
+ }
459
+ }
460
+
461
+ export function useEmojiStatus() {
462
+ onMounted(() => {
463
+ initWebApp()
464
+ })
465
+
466
+ return {
467
+ supported: computed(() => !!globalWebApp?.setEmojiStatus || !!globalWebApp?.requestEmojiStatusAccess),
468
+ set: (customEmojiId: string, params?: TelegramEmojiStatusParams) => {
469
+ return new Promise<boolean>((resolve) => {
470
+ if (!globalWebApp?.setEmojiStatus) {
471
+ resolve(false)
472
+ return
473
+ }
474
+
475
+ globalWebApp.setEmojiStatus(customEmojiId, params, resolve)
476
+ })
477
+ },
478
+ requestAccess: () => {
479
+ return new Promise<boolean>((resolve) => {
480
+ if (!globalWebApp?.requestEmojiStatusAccess) {
481
+ resolve(false)
482
+ return
483
+ }
484
+
485
+ globalWebApp.requestEmojiStatusAccess(resolve)
486
+ })
487
+ }
488
+ }
489
+ }
490
+
491
+ export function useTelegramStorage(type: 'cloud' | 'device' | 'secure' = 'cloud') {
492
+ onMounted(() => {
493
+ initWebApp()
494
+ })
495
+
496
+ const storage = computed(() => {
497
+ if (type === 'device') return globalWebApp?.DeviceStorage
498
+ if (type === 'secure') return globalWebApp?.SecureStorage
499
+ return globalWebApp?.CloudStorage
500
+ })
501
+
502
+ return {
503
+ supported: computed(() => !!storage.value),
504
+ setItem: (key: string, value: string) => {
505
+ return new Promise<boolean>((resolve, reject) => {
506
+ if (!storage.value) {
507
+ resolve(false)
508
+ return
509
+ }
510
+
511
+ storage.value.setItem(key, value, storageCallback(resolve, reject))
512
+ })
513
+ },
514
+ getItem: (key: string) => {
515
+ return new Promise<string | null>((resolve, reject) => {
516
+ if (!storage.value) {
517
+ resolve(null)
518
+ return
519
+ }
520
+
521
+ storage.value.getItem(key, storageCallback(resolve, reject))
522
+ })
523
+ },
524
+ removeItem: (key: string) => {
525
+ return new Promise<boolean>((resolve, reject) => {
526
+ if (!storage.value) {
527
+ resolve(false)
528
+ return
529
+ }
530
+
531
+ storage.value.removeItem(key, storageCallback(resolve, reject))
532
+ })
533
+ },
534
+ clear: () => {
535
+ return new Promise<boolean>((resolve, reject) => {
536
+ const currentStorage = storage.value as TelegramWebAppDeviceStorage | TelegramWebAppSecureStorage | undefined
537
+ if (!currentStorage?.clear) {
538
+ resolve(false)
539
+ return
540
+ }
541
+
542
+ currentStorage.clear(storageCallback(resolve, reject))
543
+ })
544
+ }
545
+ }
546
+ }
547
+
548
+ export function useLocationManager() {
549
+ onMounted(() => {
550
+ initWebApp()
551
+ })
552
+
553
+ return {
554
+ supported: computed(() => !!globalWebApp?.LocationManager),
555
+ inited: computed(() => globalWebApp?.LocationManager?.isInited || false),
556
+ available: computed(() => globalWebApp?.LocationManager?.isLocationAvailable || false),
557
+ accessRequested: computed(() => globalWebApp?.LocationManager?.isAccessRequested || false),
558
+ accessGranted: computed(() => globalWebApp?.LocationManager?.isAccessGranted || false),
559
+ init: () => globalWebApp?.LocationManager?.init(),
560
+ getLocation: () => {
561
+ return new Promise<TelegramLocationData | null>((resolve) => {
562
+ if (!globalWebApp?.LocationManager) {
563
+ resolve(null)
564
+ return
565
+ }
566
+
567
+ globalWebApp.LocationManager.getLocation(resolve)
568
+ })
569
+ },
570
+ openSettings: () => globalWebApp?.LocationManager?.openSettings()
571
+ }
572
+ }
573
+
574
+ export function useDeviceMotion() {
575
+ onMounted(() => {
576
+ initWebApp()
577
+ })
578
+
579
+ return {
580
+ accelerometer: computed(() => globalWebApp?.Accelerometer),
581
+ deviceOrientation: computed(() => globalWebApp?.DeviceOrientation),
582
+ gyroscope: computed(() => globalWebApp?.Gyroscope),
583
+ lockOrientation: () => globalWebApp?.lockOrientation?.(),
584
+ unlockOrientation: () => globalWebApp?.unlockOrientation?.()
585
+ }
586
+ }
587
+
299
588
  // Links and sharing
300
589
  export function openLink(url: string, options?: { try_instant_view?: boolean }) {
301
- const webApp = getWebApp()
590
+ const webApp = ensureWebApp()
302
591
  if (webApp) {
303
592
  webApp.openLink(url, options)
304
593
  } else {
@@ -308,7 +597,7 @@ export function openLink(url: string, options?: { try_instant_view?: boolean })
308
597
  }
309
598
 
310
599
  export function openTelegramLink(url: string) {
311
- const webApp = getWebApp()
600
+ const webApp = ensureWebApp()
312
601
  if (webApp) {
313
602
  webApp.openTelegramLink(url)
314
603
  } else {
@@ -318,7 +607,7 @@ export function openTelegramLink(url: string) {
318
607
  }
319
608
 
320
609
  export function shareURL(url: string) {
321
- const webApp = getWebApp()
610
+ const webApp = ensureWebApp()
322
611
  if (webApp) {
323
612
  // Use Web Share API if available, otherwise copy to clipboard
324
613
  if (navigator.share) {
@@ -340,3 +629,48 @@ export function shareURL(url: string) {
340
629
  }
341
630
  }
342
631
  }
632
+
633
+ export function shareToStory(mediaUrl: string, params?: TelegramStoryShareParams) {
634
+ const webApp = ensureWebApp()
635
+ if (webApp?.shareToStory) {
636
+ webApp.shareToStory(mediaUrl, params)
637
+ } else if (navigator.share) {
638
+ navigator.share({ url: mediaUrl }).catch(() => {})
639
+ }
640
+ }
641
+
642
+ export function shareMessage(messageId: string) {
643
+ const webApp = ensureWebApp()
644
+ return new Promise<boolean>((resolve) => {
645
+ if (!webApp?.shareMessage) {
646
+ resolve(false)
647
+ return
648
+ }
649
+
650
+ webApp.shareMessage(messageId, resolve)
651
+ })
652
+ }
653
+
654
+ export function downloadFile(params: TelegramDownloadFileParams) {
655
+ const webApp = ensureWebApp()
656
+ return new Promise<boolean>((resolve) => {
657
+ if (!webApp?.downloadFile) {
658
+ resolve(false)
659
+ return
660
+ }
661
+
662
+ webApp.downloadFile(params, resolve)
663
+ })
664
+ }
665
+
666
+ export function requestChat(reqId: string) {
667
+ const webApp = ensureWebApp()
668
+ return new Promise<boolean>((resolve) => {
669
+ if (!webApp?.requestChat) {
670
+ resolve(false)
671
+ return
672
+ }
673
+
674
+ webApp.requestChat(reqId, resolve)
675
+ })
676
+ }