@quicktvui/tv-ad-unlock 1.0.0

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/README.md ADDED
@@ -0,0 +1,155 @@
1
+ # @quicktvui/tv-ad-unlock
2
+
3
+ TV广告解锁组件,用于QuickTVUI框架的电视端广告解锁功能。
4
+
5
+ ## 功能特性
6
+
7
+ - 生成电视端广告二维码
8
+ - 轮询广告观看状态
9
+ - 自动管理解锁状态
10
+ - 支持自定义配置
11
+
12
+ ## 安装
13
+
14
+ ```bash
15
+ pnpm add @quicktvui/tv-ad-unlock
16
+ ```
17
+
18
+ ## 使用方法
19
+
20
+ ### 1. 基础使用
21
+
22
+ ```vue
23
+ <template>
24
+ <TVAdUnlock
25
+ :config="adConfig"
26
+ :storageKeys="storageKeys"
27
+ :requestManager="requestManager"
28
+ @unlockSuccess="onUnlockSuccess"
29
+ @unlockFailed="onUnlockFailed"
30
+ />
31
+ </template>
32
+
33
+ <script setup lang="ts">
34
+ import { TVAdUnlock, useTVAdUnlockStorage } from '@quicktvui/tv-ad-unlock'
35
+ import type { TVAdUnlockConfig, TVAdUnlockStorageKeys, RequestManager } from '@quicktvui/tv-ad-unlock'
36
+
37
+ const adConfig: TVAdUnlockConfig = {
38
+ packageName: 'com.example.app',
39
+ superRequestBaseUrl: 'https://superapi.extscreen.com/extscreenapi/api',
40
+ invalidTimeout: 300000,
41
+ pollInterval: 1000
42
+ }
43
+
44
+ const storageKeys: TVAdUnlockStorageKeys = {
45
+ playCountKey: 'app_play_count',
46
+ unlockDateKey: 'app_ad_unlock_date',
47
+ unlockedTodayKey: 'app_ad_unlocked_today'
48
+ }
49
+
50
+ const requestManager: RequestManager = {
51
+ post: async (url, data) => { /* 实现请求逻辑 */ },
52
+ get: async (url, data) => { /* 实现请求逻辑 */ }
53
+ }
54
+
55
+ function onUnlockSuccess() {
56
+ console.log('解锁成功')
57
+ }
58
+
59
+ function onUnlockFailed(error: Error) {
60
+ console.error('解锁失败', error)
61
+ }
62
+ </script>
63
+ ```
64
+
65
+ ### 2. 使用 Composable
66
+
67
+ ```typescript
68
+ import { useTVAdUnlock, useTVAdUnlockStorage } from '@quicktvui/tv-ad-unlock'
69
+ import { useESLocalStorage } from '@extscreen/es3-core'
70
+
71
+ const storage = useESLocalStorage()
72
+
73
+ const storageKeys = {
74
+ playCountKey: 'app_play_count',
75
+ unlockDateKey: 'app_ad_unlock_date',
76
+ unlockedTodayKey: 'app_ad_unlocked_today'
77
+ }
78
+
79
+ const adStorage = useTVAdUnlockStorage(storage, storageKeys, 32)
80
+
81
+ async function checkNeedShowAd() {
82
+ const needAd = await adStorage.checkNeedAd()
83
+ if (needAd) {
84
+ router.push({ name: 'ad' })
85
+ }
86
+ }
87
+ ```
88
+
89
+ ### 3. 路由配置
90
+
91
+ ```typescript
92
+ import { ESRouteType } from '@extscreen/es3-router'
93
+
94
+ const routes = [
95
+ {
96
+ path: '/ad',
97
+ name: 'ad',
98
+ component: () => import('./views/ad/index.vue'),
99
+ type: ESRouteType.ES_ROUTE_TYPE_DIALOG
100
+ }
101
+ ]
102
+ ```
103
+
104
+ ## API
105
+
106
+ ### Props
107
+
108
+ | 属性 | 类型 | 必填 | 默认值 | 说明 |
109
+ |------|------|------|--------|------|
110
+ | config | TVAdUnlockConfig | 是 | - | 广告配置 |
111
+ | storageKeys | TVAdUnlockStorageKeys | 是 | - | 存储键名配置 |
112
+ | requestManager | RequestManager | 是 | - | 请求管理器 |
113
+ | title | string | 否 | '观看30秒广告 当日解锁' | 标题文字 |
114
+ | subTitle | string | 否 | '手机打开【微信】扫码' | 副标题文字 |
115
+ | invalidCodeImage | string | 否 | '' | 二维码失效图片 |
116
+ | scannedCodeImage | string | 否 | '' | 已扫码图片 |
117
+ | scanTitle | string | 否 | '观看奖励' | 扫码标题 |
118
+ | scanContent | string | 否 | '观看广告后可获得奖励' | 扫码内容 |
119
+ | scanToast | string | 否 | '奖励已获得,请前往电视端观看' | 扫码提示 |
120
+
121
+ ### Events
122
+
123
+ | 事件名 | 参数 | 说明 |
124
+ |--------|------|------|
125
+ | unlockSuccess | - | 解锁成功 |
126
+ | unlockFailed | error: Error | 解锁失败 |
127
+ | qrCodeInvalid | - | 二维码失效 |
128
+ | qrCodeScanned | - | 二维码已扫码 |
129
+
130
+ ### Types
131
+
132
+ ```typescript
133
+ interface TVAdUnlockConfig {
134
+ packageName: string
135
+ superRequestBaseUrl: string
136
+ trackBaseUrl?: string
137
+ invalidTimeout?: number
138
+ pollInterval?: number
139
+ }
140
+
141
+ interface TVAdUnlockStorageKeys {
142
+ playCountKey: string
143
+ unlockDateKey: string
144
+ unlockedTodayKey: string
145
+ }
146
+
147
+ interface RequestManager {
148
+ post(url: string, data: Record<string, any>): Promise<any>
149
+ get(url: string, data: string): Promise<any>
150
+ }
151
+ ```
152
+
153
+ ## License
154
+
155
+ MIT
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@quicktvui/tv-ad-unlock",
3
+ "version": "1.0.0",
4
+ "description": "TV广告解锁组件 - 用于QuickTVUI框架的电视端广告解锁功能",
5
+ "main": "src/index.ts",
6
+ "types": "src/index.ts",
7
+ "files": [
8
+ "src"
9
+ ],
10
+ "keywords": [
11
+ "quicktvui",
12
+ "tv",
13
+ "ad",
14
+ "unlock",
15
+ "qrcode"
16
+ ],
17
+ "author": "",
18
+ "license": "MIT",
19
+ "peerDependencies": {
20
+ "vue": "^3.0.0",
21
+ "@extscreen/es3-core": ">=3.0.0",
22
+ "@extscreen/es3-router": ">=3.0.0"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": ""
27
+ }
28
+ }
@@ -0,0 +1,218 @@
1
+ <template>
2
+ <div class="ad-container">
3
+ <div class="content">
4
+ <div class="content-bg" />
5
+ <span class="title">{{ title }}</span>
6
+ <span class="sub-title">{{ subTitle }}</span>
7
+ <div class="qrcode-container">
8
+ <qt-qr-code
9
+ v-if="adQrCode && !isInvalid && qrCodeState !== 1"
10
+ class="qrcode"
11
+ :content="adQrCode"
12
+ />
13
+ <img
14
+ v-if="isInvalid"
15
+ class="invalid-code"
16
+ :src="invalidCodeImage"
17
+ />
18
+ <img
19
+ v-if="qrCodeState === 1"
20
+ class="invalid-code"
21
+ :src="scannedCodeImage"
22
+ >
23
+ </div>
24
+ </div>
25
+ </div>
26
+ </template>
27
+
28
+ <script setup lang="ts">
29
+ import { ref, computed } from 'vue'
30
+ import { ESKeyCode, useESRuntime, useESToast, useESLocalStorage } from '@extscreen/es3-core'
31
+ import { useESRouter } from '@extscreen/es3-router'
32
+ import { useTVAdUnlock, useTVAdUnlockStorage } from './useTVAdUnlock'
33
+ import type { TVAdUnlockConfig, TVAdUnlockParams, TVAdUnlockStorageKeys, RequestManager } from './types'
34
+
35
+ interface Props {
36
+ config: TVAdUnlockConfig
37
+ storageKeys: TVAdUnlockStorageKeys
38
+ requestManager: RequestManager
39
+ title?: string
40
+ subTitle?: string
41
+ invalidCodeImage?: string
42
+ scannedCodeImage?: string
43
+ scanTitle?: string
44
+ scanContent?: string
45
+ scanToast?: string
46
+ }
47
+
48
+ const props = withDefaults(defineProps<Props>(), {
49
+ title: '观看30秒广告 当日解锁',
50
+ subTitle: '手机打开【微信】扫码',
51
+ invalidCodeImage: '',
52
+ scannedCodeImage: '',
53
+ scanTitle: '观看奖励',
54
+ scanContent: '观看广告后可获得奖励',
55
+ scanToast: '奖励已获得,请前往电视端观看'
56
+ })
57
+
58
+ const emit = defineEmits<{
59
+ (e: 'unlockSuccess'): void
60
+ (e: 'unlockFailed', error: Error): void
61
+ (e: 'qrCodeInvalid'): void
62
+ (e: 'qrCodeScanned'): void
63
+ }>()
64
+
65
+ const toast = useESToast()
66
+ const storage = useESLocalStorage()
67
+ const router = useESRouter()
68
+ const runtime = useESRuntime()
69
+ const dnum = runtime.getRuntimeDeviceId()
70
+
71
+ const payParams = ref<TVAdUnlockParams>({
72
+ assetId: '',
73
+ assetName: '',
74
+ fromId: '',
75
+ fromName: ''
76
+ })
77
+
78
+ const {
79
+ adQrCode,
80
+ isInvalid,
81
+ qrCodeState,
82
+ isLoading,
83
+ isShowConfirmToast,
84
+ getAdQrCode,
85
+ stopPollTvADStatusTimer,
86
+ stopInvalidTimer,
87
+ reset
88
+ } = useTVAdUnlock(
89
+ props.config,
90
+ props.storageKeys,
91
+ props.requestManager,
92
+ {
93
+ onUnlockSuccess: () => handleUnlockSuccess(),
94
+ onUnlockFailed: (error) => emit('unlockFailed', error),
95
+ onQrCodeInvalid: () => emit('qrCodeInvalid'),
96
+ onQrCodeScanned: () => emit('qrCodeScanned')
97
+ }
98
+ )
99
+
100
+ const adStorage = useTVAdUnlockStorage(storage, props.storageKeys)
101
+
102
+ async function handleUnlockSuccess() {
103
+ await adStorage.setTodayUnlocked()
104
+ emit('unlockSuccess')
105
+ toast.showToast('解锁成功,继续观看吧!')
106
+ router.back()
107
+ }
108
+
109
+ function onKeyDown(event: any) {
110
+ switch (event.keyCode) {
111
+ case ESKeyCode.ES_KEYCODE_DPAD_CENTER:
112
+ case ESKeyCode.ES_KEYCODE_ENTER:
113
+ if (isInvalid.value || qrCodeState.value === 1) {
114
+ getAdQrCode(dnum, props.scanTitle, props.scanContent, props.scanToast)
115
+ }
116
+ break
117
+ }
118
+ }
119
+
120
+ function onESCreate(params: TVAdUnlockParams) {
121
+ payParams.value = params || { assetId: '', assetName: '', fromId: '', fromName: '' }
122
+ getAdQrCode(dnum, props.scanTitle, props.scanContent, props.scanToast)
123
+ }
124
+
125
+ function onBackPressed() {
126
+ if (!isShowConfirmToast.value && qrCodeState.value === 1) {
127
+ toast.showToast('广告播放中,请不要关闭弹窗')
128
+ isShowConfirmToast.value = true
129
+ return true
130
+ } else {
131
+ stopPollTvADStatusTimer()
132
+ stopInvalidTimer()
133
+ router.back()
134
+ }
135
+ }
136
+
137
+ function onESDestroy() {
138
+ stopPollTvADStatusTimer()
139
+ stopInvalidTimer()
140
+ }
141
+
142
+ defineExpose({
143
+ onESCreate,
144
+ onKeyDown,
145
+ onBackPressed,
146
+ onESDestroy
147
+ })
148
+ </script>
149
+
150
+ <style scoped>
151
+ .ad-container {
152
+ width: 1920px;
153
+ height: 1080px;
154
+ background-color: rgba(0, 0, 0, 0.7);
155
+ display: flex;
156
+ justify-content: center;
157
+ align-items: center;
158
+ }
159
+
160
+ .content {
161
+ width: 892px;
162
+ height: 646px;
163
+ display: flex;
164
+ flex-direction: column;
165
+ align-items: center;
166
+ justify-content: center;
167
+ }
168
+
169
+ .content-bg {
170
+ position: absolute;
171
+ width: 892px;
172
+ height: 646px;
173
+ background-color: #FBE6C4;
174
+ border-radius: 30px;
175
+ border-width: 1px;
176
+ border-color: #979797;
177
+ }
178
+
179
+ .title {
180
+ font-weight: 500;
181
+ font-size: 46px;
182
+ color: #000000;
183
+ }
184
+
185
+ .sub-title {
186
+ margin-top: 32px;
187
+ font-weight: 400;
188
+ font-size: 30px;
189
+ color: rgba(0, 0, 0, 0.65);
190
+ }
191
+
192
+ .qrcode-container {
193
+ margin-top: 32px;
194
+ width: 300px;
195
+ height: 300px;
196
+ border-radius: 30px;
197
+ background-color: #FFFFFF;
198
+ display: flex;
199
+ justify-content: center;
200
+ align-items: center;
201
+ }
202
+
203
+ .qrcode {
204
+ position: absolute;
205
+ width: 260px;
206
+ height: 260px;
207
+ border-radius: 30px;
208
+ background-color: #FFFFFF;
209
+ }
210
+
211
+ .invalid-code {
212
+ position: absolute;
213
+ width: 300px;
214
+ height: 300px;
215
+ border-radius: 30px;
216
+ background-color: transparent;
217
+ }
218
+ </style>
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './types'
2
+ export { useTVAdUnlock, useTVAdUnlockStorage } from './useTVAdUnlock'
3
+ export { default as TVAdUnlock } from './TVAdUnlock.vue'
package/src/types.ts ADDED
@@ -0,0 +1,57 @@
1
+ export interface TVAdUnlockConfig {
2
+ packageName: string
3
+ superRequestBaseUrl: string
4
+ trackBaseUrl?: string
5
+ invalidTimeout?: number
6
+ pollInterval?: number
7
+ }
8
+
9
+ export interface TVAdUnlockParams {
10
+ assetId?: string
11
+ assetName?: string
12
+ fromId?: string
13
+ fromName?: string
14
+ }
15
+
16
+ export interface TVAdUnlockCallbacks {
17
+ onUnlockSuccess?: () => void
18
+ onUnlockFailed?: (error: Error) => void
19
+ onQrCodeInvalid?: () => void
20
+ onQrCodeScanned?: () => void
21
+ }
22
+
23
+ export interface TVAdUnlockState {
24
+ adQrCode: string
25
+ isInvalid: boolean
26
+ qrCodeState: number
27
+ isLoading: boolean
28
+ }
29
+
30
+ export interface TVAdUnlockStorageKeys {
31
+ playCountKey: string
32
+ unlockDateKey: string
33
+ unlockedTodayKey: string
34
+ }
35
+
36
+ export interface TVAdUnlockOptions {
37
+ config: TVAdUnlockConfig
38
+ storageKeys: TVAdUnlockStorageKeys
39
+ freePlayCount?: number
40
+ callbacks?: TVAdUnlockCallbacks
41
+ }
42
+
43
+ export interface RequestManager {
44
+ post(url: string, data: Record<string, any>): Promise<any>
45
+ get(url: string, data: string): Promise<any>
46
+ }
47
+
48
+ export interface TrackEventParams {
49
+ id: string
50
+ name: string
51
+ pageId?: string
52
+ pageName?: string
53
+ pageSourceId?: string
54
+ pageSourceName?: string
55
+ contentId?: string
56
+ contentName?: string
57
+ }
@@ -0,0 +1,277 @@
1
+ import { ref, watch, onUnmounted } from 'vue'
2
+ import type {
3
+ TVAdUnlockConfig,
4
+ TVAdUnlockParams,
5
+ TVAdUnlockState,
6
+ TVAdUnlockStorageKeys,
7
+ TVAdUnlockCallbacks,
8
+ RequestManager
9
+ } from './types'
10
+
11
+ const DEFAULT_INVALID_TIMEOUT = 300000
12
+ const DEFAULT_POLL_INTERVAL = 1000
13
+
14
+ export function useTVAdUnlock(
15
+ config: TVAdUnlockConfig,
16
+ storageKeys: TVAdUnlockStorageKeys,
17
+ requestManager: RequestManager,
18
+ callbacks?: TVAdUnlockCallbacks
19
+ ) {
20
+ const adQrCode = ref('')
21
+ const isInvalid = ref(false)
22
+ const qrCodeState = ref(0)
23
+ const isLoading = ref(false)
24
+ const isStopPoll = ref(false)
25
+ const isShowConfirmToast = ref(false)
26
+
27
+ let invalidTimer: ReturnType<typeof setTimeout> | null = null
28
+ let pollTimer: ReturnType<typeof setTimeout> | null = null
29
+
30
+ const invalidTimeout = config.invalidTimeout ?? DEFAULT_INVALID_TIMEOUT
31
+ const pollInterval = config.pollInterval ?? DEFAULT_POLL_INTERVAL
32
+
33
+ const urlGetDeviceCode = config.superRequestBaseUrl + '/superc/ad/open/v2/tv/getAdUrl'
34
+ const urlPollTVADStatus = config.superRequestBaseUrl + '/superc/ad/open/v2/tv/pollTVADStatus'
35
+
36
+ watch(() => qrCodeState.value, (newVal, oldVal) => {
37
+ if (newVal === 1 && oldVal === 0) {
38
+ startInvalidTimer()
39
+ callbacks?.onQrCodeScanned?.()
40
+ }
41
+ })
42
+
43
+ function getTodayString(): string {
44
+ const today = new Date()
45
+ return `${today.getFullYear()}-${today.getMonth() + 1}-${today.getDate()}`
46
+ }
47
+
48
+ async function isTodayUnlocked(storage: { getString: (key: string, defaultValue?: string) => Promise<string> }): Promise<boolean> {
49
+ const today = getTodayString()
50
+ const unlockedDate = await storage.getString(storageKeys.unlockedTodayKey, '')
51
+ return unlockedDate === today
52
+ }
53
+
54
+ async function getTodayPlayCount(storage: { getString: (key: string, defaultValue?: string) => Promise<string>; putString: (key: string, value: string) => Promise<void> }): Promise<number> {
55
+ const today = getTodayString()
56
+ const savedDate = await storage.getString(storageKeys.unlockDateKey, '')
57
+
58
+ if (savedDate !== today) {
59
+ await storage.putString(storageKeys.unlockDateKey, today)
60
+ await storage.putString(storageKeys.playCountKey, '0')
61
+ return 0
62
+ }
63
+
64
+ const count = await storage.getString(storageKeys.playCountKey, '0')
65
+ return parseInt(count) || 0
66
+ }
67
+
68
+ async function incrementPlayCount(storage: { getString: (key: string, defaultValue?: string) => Promise<string>; putString: (key: string, value: string) => Promise<void> }): Promise<number> {
69
+ const count = await getTodayPlayCount(storage)
70
+ const newCount = count + 1
71
+ await storage.putString(storageKeys.playCountKey, String(newCount))
72
+ return newCount
73
+ }
74
+
75
+ async function checkNeedAd(storage: { getString: (key: string, defaultValue?: string) => Promise<string>; putString: (key: string, value: string) => Promise<void> }, freePlayCount: number): Promise<boolean> {
76
+ const unlocked = await isTodayUnlocked(storage)
77
+ if (unlocked) {
78
+ return false
79
+ }
80
+ const count = await getTodayPlayCount(storage)
81
+ return count >= freePlayCount
82
+ }
83
+
84
+ async function pollTVADStatus(deviceId: string): Promise<void> {
85
+ try {
86
+ const res = await requestManager.post(urlPollTVADStatus, {
87
+ data: { deviceId }
88
+ })
89
+
90
+ if (isStopPoll.value) {
91
+ return
92
+ }
93
+
94
+ qrCodeState.value = parseInt(`${res}`)
95
+
96
+ if (qrCodeState.value === 2) {
97
+ callbacks?.onUnlockSuccess?.()
98
+ } else {
99
+ pollTimer = setTimeout(() => {
100
+ pollTVADStatus(deviceId)
101
+ }, pollInterval)
102
+ }
103
+ } catch {
104
+ pollTimer = setTimeout(() => {
105
+ pollTVADStatus(deviceId)
106
+ }, pollInterval)
107
+ }
108
+ }
109
+
110
+ async function getAdQrCode(deviceId: string, scanTitle: string = '观看奖励', scanContent: string = '观看广告后可获得奖励', scanToast: string = '奖励已获得,请前往电视端观看'): Promise<string> {
111
+ isLoading.value = true
112
+ isInvalid.value = false
113
+ qrCodeState.value = 0
114
+
115
+ try {
116
+ const res = await requestManager.post(urlGetDeviceCode, {
117
+ data: {
118
+ deviceId,
119
+ scanTitle,
120
+ scanContent,
121
+ scanToast
122
+ }
123
+ })
124
+
125
+ if (res) {
126
+ adQrCode.value = res
127
+ isInvalid.value = false
128
+ startPollTvADStatusTimer(deviceId)
129
+ startInvalidTimer()
130
+ isLoading.value = false
131
+ return res
132
+ } else {
133
+ isLoading.value = false
134
+ callbacks?.onUnlockFailed?.(new Error('获取二维码失败'))
135
+ return ''
136
+ }
137
+ } catch (e) {
138
+ isLoading.value = false
139
+ callbacks?.onUnlockFailed?.(e as Error)
140
+ return ''
141
+ }
142
+ }
143
+
144
+ function startPollTvADStatusTimer(deviceId: string): void {
145
+ stopPollTvADStatusTimer()
146
+ isStopPoll.value = false
147
+ pollTimer = setTimeout(() => {
148
+ pollTVADStatus(deviceId)
149
+ }, pollInterval)
150
+ }
151
+
152
+ function stopPollTvADStatusTimer(): void {
153
+ if (pollTimer) {
154
+ clearTimeout(pollTimer)
155
+ pollTimer = null
156
+ isStopPoll.value = true
157
+ }
158
+ }
159
+
160
+ function stopInvalidTimer(): void {
161
+ if (invalidTimer) {
162
+ clearTimeout(invalidTimer)
163
+ invalidTimer = null
164
+ }
165
+ }
166
+
167
+ function startInvalidTimer(): void {
168
+ stopInvalidTimer()
169
+ invalidTimer = setTimeout(() => {
170
+ invalidQrCode()
171
+ invalidTimer = null
172
+ }, invalidTimeout)
173
+ }
174
+
175
+ function invalidQrCode(): void {
176
+ isInvalid.value = true
177
+ stopPollTvADStatusTimer()
178
+ callbacks?.onQrCodeInvalid?.()
179
+ }
180
+
181
+ function reset(): void {
182
+ adQrCode.value = ''
183
+ isInvalid.value = false
184
+ qrCodeState.value = 0
185
+ isLoading.value = false
186
+ isStopPoll.value = false
187
+ isShowConfirmToast.value = false
188
+ stopPollTvADStatusTimer()
189
+ stopInvalidTimer()
190
+ }
191
+
192
+ onUnmounted(() => {
193
+ stopPollTvADStatusTimer()
194
+ stopInvalidTimer()
195
+ })
196
+
197
+ return {
198
+ adQrCode,
199
+ isInvalid,
200
+ qrCodeState,
201
+ isLoading,
202
+ isShowConfirmToast,
203
+ getAdQrCode,
204
+ startPollTvADStatusTimer,
205
+ stopPollTvADStatusTimer,
206
+ startInvalidTimer,
207
+ stopInvalidTimer,
208
+ invalidQrCode,
209
+ reset,
210
+ isTodayUnlocked,
211
+ getTodayPlayCount,
212
+ incrementPlayCount,
213
+ checkNeedAd
214
+ }
215
+ }
216
+
217
+ export function useTVAdUnlockStorage(
218
+ storage: { getString: (key: string, defaultValue?: string) => Promise<string>; putString: (key: string, value: string) => Promise<void> },
219
+ storageKeys: TVAdUnlockStorageKeys,
220
+ freePlayCount: number = 32
221
+ ) {
222
+ function getTodayString(): string {
223
+ const today = new Date()
224
+ return `${today.getFullYear()}-${today.getMonth() + 1}-${today.getDate()}`
225
+ }
226
+
227
+ async function isTodayUnlocked(): Promise<boolean> {
228
+ const today = getTodayString()
229
+ const unlockedDate = await storage.getString(storageKeys.unlockedTodayKey, '')
230
+ return unlockedDate === today
231
+ }
232
+
233
+ async function getTodayPlayCount(): Promise<number> {
234
+ const today = getTodayString()
235
+ const savedDate = await storage.getString(storageKeys.unlockDateKey, '')
236
+
237
+ if (savedDate !== today) {
238
+ await storage.putString(storageKeys.unlockDateKey, today)
239
+ await storage.putString(storageKeys.playCountKey, '0')
240
+ return 0
241
+ }
242
+
243
+ const count = await storage.getString(storageKeys.playCountKey, '0')
244
+ return parseInt(count) || 0
245
+ }
246
+
247
+ async function incrementPlayCount(): Promise<number> {
248
+ const count = await getTodayPlayCount()
249
+ const newCount = count + 1
250
+ await storage.putString(storageKeys.playCountKey, String(newCount))
251
+ return newCount
252
+ }
253
+
254
+ async function checkNeedAd(): Promise<boolean> {
255
+ const unlocked = await isTodayUnlocked()
256
+ if (unlocked) {
257
+ return false
258
+ }
259
+ const count = await getTodayPlayCount()
260
+ return count >= freePlayCount
261
+ }
262
+
263
+ async function setTodayUnlocked(): Promise<void> {
264
+ const today = getTodayString()
265
+ await storage.putString(storageKeys.unlockedTodayKey, today)
266
+ await storage.putString(storageKeys.playCountKey, '0')
267
+ await storage.putString(storageKeys.unlockDateKey, today)
268
+ }
269
+
270
+ return {
271
+ isTodayUnlocked,
272
+ getTodayPlayCount,
273
+ incrementPlayCount,
274
+ checkNeedAd,
275
+ setTodayUnlocked
276
+ }
277
+ }