fix: 首页弹窗报错

This commit is contained in:
guanchao
2026-11-06 21:27:58 +08:00
parent 553a3f0ced
commit ac02cffa1e
2 changed files with 59 additions and 10 deletions
+16 -10
View File
@@ -54,8 +54,8 @@ export default class HomePopupQueue extends cc.Component {
if (this.policy.record.monthlyRewardPending) this.policy.request('monthlyReward');
HomePopupQueue.loginPending = false;
}
completePreparation(source: string): void { this.sources.delete(source); }
holdPreparation(source: string): void { if (this.collecting) this.sources.add(source); }
completePreparation(source: string): void { if (this.sources) this.sources.delete(source); }
holdPreparation(source: string): void { if (this.collecting && this.sources) this.sources.add(source); }
start(): void {
if (!this.node.getChildByName('sevenDayGift')) this.completePreparation('seven');
}
@@ -66,26 +66,28 @@ export default class HomePopupQueue extends cc.Component {
&& this.policy.eligible(key, typeof type === 'function' ? type() : type);
}
unavailable(key: string): void {
if (this.policy.ended || this.recordKey !== HomePopupQueue.storageKey()
if (!this.policy || this.policy.ended || this.recordKey !== HomePopupQueue.storageKey()
|| this.accountToken !== cc.fx.GameConfig.GM_INFO.token || this.session !== HomePopupQueue.sessions[this.recordKey]) return;
this.policy.unavailable(key);
}
requestPresentation(key: string): boolean {
if (this.policy.ended || this.recordKey !== HomePopupQueue.storageKey()
if (!this.policy || this.policy.ended || this.recordKey !== HomePopupQueue.storageKey()
|| this.accountToken !== cc.fx.GameConfig.GM_INFO.token || this.session !== HomePopupQueue.sessions[this.recordKey]) return false;
return this.policy.request(key);
}
wasShownToday(key: string): boolean { return !!this.policy.record.shown[key]; }
wasShownToday(key: string): boolean { return !!this.policy && !!this.policy.record.shown[key]; }
markShown(key: string): void { if (this.isRunning(key)) this.policy.shown(key); }
isRunning(key: string): boolean {
return !this.policy.ended && this.running === key && !cc.fx.GameConfig.isGameplayEntryPreparing()
// Late network/load callbacks may outlive Cocos clearing the component's object fields.
return !!this.policy && !this.policy.ended && this.running === key && !cc.fx.GameConfig.isGameplayEntryPreparing()
&& this.recordKey === HomePopupQueue.storageKey() && this.accountToken === cc.fx.GameConfig.GM_INFO.token
&& this.session === HomePopupQueue.sessions[this.recordKey];
}
/** A manual popup may have opened while this automatic task was loading. */
canPresent(key: string): boolean { return this.isRunning(key) && !this.hasVisiblePopup(key); }
get monthlyRewardPending(): boolean { return !!this.policy.record.monthlyRewardPending; }
get monthlyRewardPending(): boolean { return !!this.policy && !!this.policy.record.monthlyRewardPending; }
setMonthlyRewardPending(): void {
if (!this.policy) return;
this.policy.record.monthlyRewardPending = true;
const current: PopupRecord = cc.fx.StorageMessage.getStorage(this.recordKey) || this.policy.record;
current.monthlyRewardPending = true;
@@ -93,14 +95,17 @@ export default class HomePopupQueue extends cc.Component {
this.requestPresentation('monthlyReward');
}
cancelBatch(): void {
this.policy.end(); this.collecting = false; this.tasks.length = 0; this.running = '';
if (this.policy) this.policy.end();
this.collecting = false;
if (this.tasks) this.tasks.length = 0;
this.running = '';
const cancel = this.cancelRun; this.cancelRun = null;
try { if (cancel) cancel(); }
finally { if (this.backdrop && cc.isValid(this.backdrop, true)) this.backdrop.release(this); }
}
public has(key: string): boolean {
return this.running === key || this.tasks.some(task => task.key === key);
return this.running === key || !!this.tasks && this.tasks.some(task => task.key === key);
}
public static get(canvas: cc.Node): HomePopupQueue {
@@ -108,9 +113,10 @@ export default class HomePopupQueue extends cc.Component {
}
public enqueue(key: string, priority: number, run: PopupRun, type?: PopupType): boolean {
if (!this.policy || this.policy.ended) return false;
const managed = !!HOME_POPUP_RULES[key];
if (managed && !this.canCollect(key, type)) return false;
if (this.policy.ended || this.running === key || this.tasks.some(task => task.key === key)) return false;
if (this.running === key || this.tasks.some(task => task.key === key)) return false;
// Keep accepted daily/login candidates pending until actual visibility, including load failures.
this.policy.request(key, typeof type === 'function' ? type() : type);
priority = key === 'ordersAfterPassSettlement' ? 33.25
+43
View File
@@ -236,6 +236,49 @@ test('relogin invalidates old queue callbacks even when account and token are un
assert.equal(current.policy.hasPending('starterPack'), true, 'old callbacks cannot discard a current pending request');
});
test('late task callbacks are harmless after destroyed component fields are cleared', () => {
const h = harness(), old = h.create();
let finish, continueBackdrop, cancelled = 0;
old.enqueue('starterPack', 0, (done, keepBackdrop) => {
finish = done; continueBackdrop = keepBackdrop;
return () => { cancelled++; done(); };
});
h.ready(old); old.update(); old.onDestroy();
assert.equal(cancelled, 1);
// Cocos clears object references after onDestroy; requests still retain their callbacks.
for (const key of ['policy', 'sources', 'tasks', 'session', 'backdrop', 'node']) old[key] = null;
const current = h.create(false), saved = structuredClone([...h.storage]);
finish(); continueBackdrop(); old.markShown('starterPack'); old.unavailable('starterPack');
old.completePreparation('starter'); old.holdPreparation('starter');
old.setMonthlyRewardPending(); old.cancelBatch(); old.update();
assert.equal(old.requestPresentation('starterPack'), false);
assert.equal(old.canCollect('starterPack'), false);
assert.equal(old.isRunning('starterPack'), false);
assert.equal(old.canPresent('starterPack'), false);
assert.equal(old.wasShownToday('starterPack'), false);
assert.equal(old.monthlyRewardPending, false);
assert.equal(old.has('starterPack'), false);
for (const key of ['starterPack', 'ordersAfterPassSettlement'])
assert.equal(old.enqueue(key, 0, () => assert.fail('destroyed task ran')), false);
assert.deepEqual([...h.storage], saved, 'late callbacks cannot change persisted records');
assert.equal(current.policy.hasPending('starterPack'), true);
assert.equal(current.enqueue('starterPack', 0, done => { current.markShown('starterPack'); done(); }), true);
h.ready(current); current.update();
assert.equal(current.wasShownToday('starterPack'), true, 'the next home can retry the unfinished popup');
});
test('queue callbacks before onLoad reject work without an initialized policy', () => {
const h = harness(), q = new h.Queue();
q.unavailable('starterPack'); q.markShown('starterPack'); q.setMonthlyRewardPending();
assert.equal(q.requestPresentation('starterPack'), false);
assert.equal(q.isRunning('starterPack'), false);
assert.equal(q.wasShownToday('starterPack'), false);
assert.equal(q.monthlyRewardPending, false);
assert.equal(q.enqueue('ordersAfterPassSettlement', 0, () => assert.fail('uninitialized task ran')), false);
q.onDestroy();
assert.equal(h.storage.size, 0);
});
for (const scenario of ['four_failures', 'level_purchase', 'leave_during_activation']) test(`lucky offer: deferred ${scenario} activation returns through the real home adapter`, () => {
const reason = scenario === 'leave_during_activation' ? 'four_failures' : scenario;
const h = harness(), first = h.create(); h.ready(first); first.update(); first.onDestroy();