Skip to content

Commit 3e506c1

Browse files
committed
chore: entitlements refresh logic every 10 minutes for promo end detection
1 parent 77e1c26 commit 3e506c1

File tree

2 files changed

+160
-7
lines changed

2 files changed

+160
-7
lines changed

src/services/login-service.js

Lines changed: 158 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ define(function (require, exports, module) {
2727
require("./setup-login-service"); // this adds loginService to KernalModeTrust
2828
require("./promotions");
2929

30+
const Metrics = require("utils/Metrics");
31+
3032
const MS_IN_DAY = 10 * 24 * 60 * 60 * 1000;
33+
const TEN_MINUTES = 10 * 60 * 1000;
3134

3235
const KernalModeTrust = window.KernalModeTrust;
3336
if(!KernalModeTrust){
@@ -43,6 +46,25 @@ define(function (require, exports, module) {
4346
// Cached entitlements data
4447
let cachedEntitlements = null;
4548

49+
// Last recorded state for entitlements monitoring
50+
let lastRecordedState = null;
51+
52+
// Debounced trigger for entitlements changed
53+
let entitlementsChangedTimer = null;
54+
55+
function _debounceEntitlementsChanged() {
56+
if (entitlementsChangedTimer) {
57+
// already scheduled, skip
58+
return;
59+
}
60+
61+
entitlementsChangedTimer = setTimeout(() => {
62+
LoginService.trigger(EVENT_ENTITLEMENTS_CHANGED);
63+
entitlementsChangedTimer = null;
64+
}, 1000); // atmost 1 entitlement changed event will be triggered in a second
65+
}
66+
67+
4668
/**
4769
* Get entitlements from API or cache
4870
* Returns null if user is not logged in
@@ -96,7 +118,7 @@ define(function (require, exports, module) {
96118

97119
// Trigger event if entitlements changed
98120
if (entitlementsChanged) {
99-
LoginService.trigger(EVENT_ENTITLEMENTS_CHANGED, result);
121+
_debounceEntitlementsChanged();
100122
}
101123

102124
return cachedEntitlements;
@@ -116,12 +138,139 @@ define(function (require, exports, module) {
116138
function clearEntitlements() {
117139
if (cachedEntitlements) {
118140
cachedEntitlements = null;
141+
_debounceEntitlementsChanged();
142+
}
143+
}
144+
145+
/**
146+
* Check if any validTill time has expired
147+
*/
148+
function validTillExpired(entitlements, lastRecordedEntitlement) {
149+
if (!entitlements) {
150+
return null;
151+
}
152+
153+
const now = Date.now();
154+
155+
function isNewlyExpired(validTill, lastValidTill) {
156+
return (
157+
validTill &&
158+
validTill < now && // expired now
159+
(!lastValidTill || lastValidTill >= now) // but wasn't expired before
160+
);
161+
}
162+
163+
// Check plan validTill
164+
if (entitlements.plan) {
165+
const validTill = entitlements.plan.validTill;
166+
const lastValidTill = (lastRecordedEntitlement && lastRecordedEntitlement.plan)
167+
? lastRecordedEntitlement.plan.validTill
168+
: null;
169+
170+
if (isNewlyExpired(validTill, lastValidTill)) {
171+
return entitlements.plan.name || brackets.config.main_pro_plan;
172+
}
173+
}
174+
175+
// Check entitlements validTill
176+
if (entitlements.entitlements) {
177+
for (const key in entitlements.entitlements) {
178+
const entitlement = entitlements.entitlements[key];
179+
if (!entitlement) {
180+
continue;
181+
}
182+
183+
const validTill = entitlement.validTill;
184+
const lastValidTill = (lastRecordedEntitlement &&
185+
lastRecordedEntitlement.entitlements &&
186+
lastRecordedEntitlement.entitlements[key])
187+
? lastRecordedEntitlement.entitlements[key].validTill
188+
: null;
189+
190+
if (isNewlyExpired(validTill, lastValidTill)) {
191+
return key;
192+
}
193+
}
194+
}
195+
196+
return null;
197+
}
198+
199+
200+
/**
201+
* Check if entitlements have changed from last recorded state
202+
*/
203+
function haveEntitlementsChanged(current, last) {
204+
if (!last && !current) {
205+
return false;
206+
}
207+
if ((!last && current) || (!current && last)) {
208+
return true;
209+
}
210+
if ((!last.entitlements && current.entitlements) || (current.entitlements && !last.entitlements)) {
211+
return true;
212+
}
213+
214+
// Check paidSubscriber changes
215+
const currentPaidSub = current.plan && current.plan.paidSubscriber;
216+
const lastPaidSub = last.plan && last.plan.paidSubscriber;
217+
if (currentPaidSub !== lastPaidSub) {
218+
return true;
219+
}
220+
221+
// Check plan name changes
222+
const currentPlanName = current.plan && current.plan.name;
223+
const lastPlanName = last.plan && last.plan.name;
224+
if (currentPlanName !== lastPlanName) {
225+
return true;
226+
}
119227

120-
// Trigger event when entitlements are cleared
121-
if (LoginService.trigger) {
122-
LoginService.trigger(EVENT_ENTITLEMENTS_CHANGED, null);
228+
// Check entitlement activations
229+
if (current.entitlements && last.entitlements) {
230+
for (const key of Object.keys(current.entitlements)) {
231+
const currentActivated = current.entitlements[key] && current.entitlements[key].activated;
232+
const lastActivated = last.entitlements[key] && last.entitlements[key].activated;
233+
if (currentActivated !== lastActivated) {
234+
return true;
235+
}
123236
}
124237
}
238+
239+
return false;
240+
}
241+
242+
/**
243+
* Start the 10-minute interval timer for monitoring entitlements
244+
*/
245+
function startEntitlementsMonitor() {
246+
setInterval(async () => {
247+
try {
248+
const current = await getEffectiveEntitlements(false); // Get effective entitlements
249+
250+
// Check if we need to refresh
251+
const expiredPlanName = validTillExpired(current, lastRecordedState);
252+
const hasChanged = haveEntitlementsChanged(current, lastRecordedState);
253+
254+
if (expiredPlanName || hasChanged) {
255+
console.log(`Entitlements monitor detected changes, Expired: ${expiredPlanName},` +
256+
`changed: ${hasChanged} refreshing...`);
257+
Metrics.countEvent(Metrics.EVENT_TYPE.PRO, "entRefresh",
258+
expiredPlanName ? "exp_"+expiredPlanName : "changed");
259+
await getEffectiveEntitlements(true); // Force refresh
260+
// if not logged in, the getEffectiveEntitlements will not trigger change even if some trial
261+
// entitlements changed. so we trigger a change anyway here. The debounce will take care of
262+
// multi fire and we are ok with multi fire 1 second apart.
263+
_debounceEntitlementsChanged();
264+
}
265+
266+
// Update last recorded state
267+
lastRecordedState = current;
268+
} catch (error) {
269+
console.error('Entitlements monitor error:', error);
270+
}
271+
}, TEN_MINUTES);
272+
273+
console.log('Entitlements monitor started (10-minute interval)');
125274
}
126275

127276
/**
@@ -197,7 +346,8 @@ define(function (require, exports, module) {
197346
* @example
198347
* // Listen for entitlements changes
199348
* const LoginService = window.KernelModeTrust.loginService;
200-
* LoginService.on(LoginService.EVENT_ENTITLEMENTS_CHANGED, (entitlements) => {
349+
* LoginService.on(LoginService.EVENT_ENTITLEMENTS_CHANGED, async() => {
350+
* const entitlements = await LoginService.getEffectiveEntitlements();
201351
* console.log('Entitlements changed:', entitlements);
202352
* // Update UI based on new entitlements
203353
* });
@@ -278,4 +428,7 @@ define(function (require, exports, module) {
278428
LoginService.getEffectiveEntitlements = getEffectiveEntitlements;
279429
LoginService.clearEntitlements = clearEntitlements;
280430
LoginService.EVENT_ENTITLEMENTS_CHANGED = EVENT_ENTITLEMENTS_CHANGED;
431+
432+
// Start the entitlements monitor timer
433+
startEntitlementsMonitor();
281434
});

src/services/promotions.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,8 +292,8 @@ define(function (require, exports, module) {
292292

293293
// Also trigger entitlements changed event since effective entitlements have changed
294294
// This allows UI components to update based on the new trial status
295-
const effectiveEntitlements = await LoginService.getEffectiveEntitlements();
296-
LoginService.trigger(LoginService.EVENT_ENTITLEMENTS_CHANGED, effectiveEntitlements);
295+
await LoginService.getEffectiveEntitlements();
296+
LoginService.trigger(LoginService.EVENT_ENTITLEMENTS_CHANGED);
297297
}
298298

299299
function _isAnyDialogsVisible() {

0 commit comments

Comments
 (0)