Skip to content

Commit 9e72eba

Browse files
committed
feat(donation-popup): implement progressive delay for donation popup based on show count
1 parent 7ff0202 commit 9e72eba

File tree

1 file changed

+47
-14
lines changed

1 file changed

+47
-14
lines changed

dashboard/src/components/common/donation-popup.tsx

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,34 @@ import { cn } from '@/lib/utils'
66
import { getAuthToken } from '@/utils/authStorage'
77

88
const DONATION_STORAGE_KEY = 'donation_popup_data'
9-
const DAYS_BETWEEN_SHOWS = 3
109
const FIRST_SHOW_DELAY = 10 * 60 * 1000 // 10 minutes in milliseconds
1110
const SUBSEQUENT_SHOW_DELAY = 5000 // 5 seconds for subsequent shows
1211
const SECRET_SALT = 'pasarguard_donation_v1' // Simple salt for checksum
1312

1413
interface DonationData {
1514
lastShown: string | null
1615
nextShowTime: string
16+
showCount: number
1717
checksum: string
1818
}
1919

20+
// Calculate delay in days based on show count (progressive delays)
21+
const getDelayDays = (showCount: number): number => {
22+
switch (showCount) {
23+
case 0:
24+
return 3 // First show: 3 days
25+
case 1:
26+
return 7 // Second show: 7 days
27+
case 2:
28+
return 14 // Third show: 14 days
29+
default:
30+
return 30 // Fourth show and beyond: 30 days (1 month)
31+
}
32+
}
33+
2034
// Simple hash function for tamper detection
21-
const generateChecksum = (lastShown: string | null, nextShowTime: string): string => {
22-
const data = `${lastShown || 'null'}_${nextShowTime}_${SECRET_SALT}`
35+
const generateChecksum = (lastShown: string | null, nextShowTime: string, showCount: number): string => {
36+
const data = `${lastShown || 'null'}_${nextShowTime}_${showCount}_${SECRET_SALT}`
2337
let hash = 0
2438
for (let i = 0; i < data.length; i++) {
2539
const char = data.charCodeAt(i)
@@ -32,12 +46,18 @@ const generateChecksum = (lastShown: string | null, nextShowTime: string): strin
3246
// Validate data integrity and reasonableness
3347
const validateData = (data: DonationData): boolean => {
3448
// Check checksum
35-
const expectedChecksum = generateChecksum(data.lastShown, data.nextShowTime)
49+
const expectedChecksum = generateChecksum(data.lastShown, data.nextShowTime, data.showCount ?? 0)
3650
if (data.checksum !== expectedChecksum) {
3751
console.warn('Donation popup: Data tampering detected (checksum mismatch)')
3852
return false
3953
}
4054

55+
// Validate showCount is a non-negative integer
56+
if (typeof data.showCount !== 'number' || data.showCount < 0 || !Number.isInteger(data.showCount)) {
57+
console.warn('Donation popup: Invalid showCount')
58+
return false
59+
}
60+
4161
// Validate timestamps are valid dates
4262
const nextShowTimestamp = new Date(data.nextShowTime).getTime()
4363
if (isNaN(nextShowTimestamp)) {
@@ -58,8 +78,9 @@ const validateData = (data: DonationData): boolean => {
5878
return false
5979
}
6080

61-
// Validate: nextShowTime shouldn't be more than 4 days after lastShown (max 3 days + 1 day buffer)
62-
const maxExpectedNext = lastShownTimestamp + 4 * 24 * 60 * 60 * 1000
81+
// Validate: nextShowTime should match expected delay based on showCount
82+
const expectedDelayDays = getDelayDays(data.showCount)
83+
const maxExpectedNext = lastShownTimestamp + (expectedDelayDays + 1) * 24 * 60 * 60 * 1000 // +1 day buffer
6384
if (nextShowTimestamp > maxExpectedNext) {
6485
console.warn('Donation popup: nextShowTime too far in future')
6586
return false
@@ -81,7 +102,7 @@ const validateData = (data: DonationData): boolean => {
81102

82103
// localStorage helper functions
83104
const setStorageData = (data: Omit<DonationData, 'checksum'>) => {
84-
const checksum = generateChecksum(data.lastShown, data.nextShowTime)
105+
const checksum = generateChecksum(data.lastShown, data.nextShowTime, data.showCount ?? 0)
85106
const fullData: DonationData = { ...data, checksum }
86107
localStorage.setItem(DONATION_STORAGE_KEY, JSON.stringify(fullData))
87108
}
@@ -90,14 +111,19 @@ const getStorageData = (): DonationData | null => {
90111
const stored = localStorage.getItem(DONATION_STORAGE_KEY)
91112
if (!stored) return null
92113
try {
93-
const data = JSON.parse(stored) as DonationData
114+
const data = JSON.parse(stored) as Partial<DonationData>
115+
// Handle backward compatibility: if showCount is missing, default to 0
116+
const fullData: DonationData = {
117+
...data,
118+
showCount: data.showCount ?? 0,
119+
} as DonationData
94120
// Validate data integrity
95-
if (!validateData(data)) {
121+
if (!validateData(fullData)) {
96122
// Tampering detected, clear invalid data
97123
localStorage.removeItem(DONATION_STORAGE_KEY)
98124
return null
99125
}
100-
return data
126+
return fullData
101127
} catch {
102128
return null
103129
}
@@ -110,11 +136,18 @@ export default function DonationPopup() {
110136

111137
const showPopup = useCallback(() => {
112138
const now = Date.now()
113-
// Update storage: set lastShown to now and nextShowTime to 3 days from now
114-
const nextShowTime = new Date(now + DAYS_BETWEEN_SHOWS * 24 * 60 * 60 * 1000).toISOString()
139+
const data = getStorageData()
140+
const currentShowCount = data?.showCount ?? 0
141+
142+
// Calculate next delay based on current show count
143+
const delayDays = getDelayDays(currentShowCount)
144+
const nextShowTime = new Date(now + delayDays * 24 * 60 * 60 * 1000).toISOString()
145+
146+
// Update storage: increment showCount and set nextShowTime based on progressive delay
115147
setStorageData({
116148
lastShown: new Date(now).toISOString(),
117149
nextShowTime,
150+
showCount: currentShowCount + 1,
118151
})
119152

120153
// Make visible immediately
@@ -139,9 +172,9 @@ export default function DonationPopup() {
139172
const now = Date.now()
140173

141174
if (!data) {
142-
// First time - schedule for 1 hour from now and store it
175+
// First time - schedule for initial delay and store it with showCount 0
143176
const nextShowTime = new Date(now + FIRST_SHOW_DELAY).toISOString()
144-
setStorageData({ lastShown: null, nextShowTime })
177+
setStorageData({ lastShown: null, nextShowTime, showCount: 0 })
145178
setTimeout(() => showPopup(), FIRST_SHOW_DELAY)
146179
return
147180
}

0 commit comments

Comments
 (0)