Skip to content

Commit d4583ff

Browse files
committed
feat: polish store purchase preview and activation flows
1 parent 2ac0919 commit d4583ff

59 files changed

Lines changed: 3486 additions & 864 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

native/StorePurchaseAddon/addon.cpp

Lines changed: 523 additions & 8 deletions
Large diffs are not rendered by default.
Lines changed: 112 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import assert from 'node:assert/strict';
2+
import fs from 'node:fs/promises';
3+
import path from 'node:path';
24
import { describe, it } from 'node:test';
35
import { HAGICODE_SPONSOR_PLAN_STORE_ID } from '../../types/subscription.js';
46
import { HAGICODE_TURBOENGINE_STORE_ID, turboEngineProductConfig } from '../../types/turboengine-license.js';
@@ -7,7 +9,9 @@ import type {
79
RawStoreSubscriptionState,
810
SubscriptionPlatformBroker,
911
} from '../subscription/subscription-broker.js';
10-
import { buildSupportedStateFromMinimalStoreApis, MicrosoftStoreSubscriptionBroker } from '../subscription/subscription-broker.js';
12+
import { buildSupportedStateFromProductQueries, MicrosoftStoreSubscriptionBroker } from '../subscription/subscription-broker.js';
13+
14+
const brokerPath = path.resolve(process.cwd(), 'src/main/subscription/subscription-broker.ts');
1115

1216
class MockPlatformBroker implements SubscriptionPlatformBroker {
1317
constructor(
@@ -32,8 +36,24 @@ function toWinRtDateTime(isoString: string): { universalTime: bigint } {
3236
};
3337
}
3438

39+
function createProductQueryResult(products: Record<string, Record<string, unknown>>): {
40+
products: {
41+
hasKey: (key: string) => boolean;
42+
get: (key: string) => Record<string, unknown> | undefined;
43+
};
44+
extendedError: 0;
45+
} {
46+
return {
47+
products: {
48+
hasKey: (key) => Object.prototype.hasOwnProperty.call(products, key),
49+
get: (key) => products[key],
50+
},
51+
extendedError: 0,
52+
};
53+
}
54+
3555
describe('subscription broker', () => {
36-
it('passes the native window handle to the dynwinrt adapter as a bigint', async () => {
56+
it('passes the native window handle to the broker adapter as a bigint', async () => {
3757
let observedHandle: bigint | null = null;
3858

3959
const broker = new MicrosoftStoreSubscriptionBroker({
@@ -67,83 +87,124 @@ describe('subscription broker', () => {
6787
assert.equal(observedHandle, 0x12345678n);
6888
});
6989

70-
it('returns an unavailable snapshot when dynwinrt initialization fails', async () => {
90+
it('returns an unavailable snapshot when native addon broker initialization fails', async () => {
7191
const broker = new MicrosoftStoreSubscriptionBroker({
7292
adapterFactory: async () => {
73-
throw new Error('dynwinrt bindings missing');
93+
throw new Error('native addon missing');
7494
},
7595
});
7696

7797
const status = await broker.queryStatus();
7898
const purchase = await broker.purchase();
7999

80100
assert.equal(status.availability, 'store-unavailable');
81-
assert.match(status.errorMessage ?? '', /dynwinrt bindings missing/);
101+
assert.match(status.errorMessage ?? '', /native addon missing/);
82102
assert.equal(purchase.outcome, 'not-supported');
83-
assert.match(purchase.errorMessage ?? '', /dynwinrt bindings missing/);
103+
assert.match(purchase.errorMessage ?? '', /native addon missing/);
84104
});
85105

86-
it('builds a supported snapshot from license and eligibility APIs without product queries', () => {
106+
it('builds a supported subscription snapshot from associated and collection product queries', () => {
87107
const expirationDate = '2026-07-01T00:00:00.000Z';
88-
const state = buildSupportedStateFromMinimalStoreApis({
108+
const state = buildSupportedStateFromProductQueries({
89109
fetchedAt: '2026-06-14T06:00:00.000Z',
90-
appLicense: {
91-
isActive: true,
92-
addOnLicenses: {
93-
hasKey: (key) => key === HAGICODE_SPONSOR_PLAN_STORE_ID,
94-
get: (key) => (key === HAGICODE_SPONSOR_PLAN_STORE_ID
95-
? {
96-
skuStoreId: HAGICODE_SPONSOR_PLAN_STORE_ID,
97-
isActive: true,
98-
expirationDate: toWinRtDateTime(expirationDate),
99-
}
100-
: undefined),
110+
associatedQueryResult: createProductQueryResult({
111+
[HAGICODE_SPONSOR_PLAN_STORE_ID]: {
112+
storeId: HAGICODE_SPONSOR_PLAN_STORE_ID,
113+
title: 'Hagicode Sponsor Plan',
114+
isInUserCollection: false,
115+
skus: [
116+
{
117+
storeId: `${HAGICODE_SPONSOR_PLAN_STORE_ID}/0010`,
118+
title: 'Monthly subscription',
119+
isSubscription: true,
120+
isInUserCollection: false,
121+
collectionData: {
122+
endDate: toWinRtDateTime(expirationDate),
123+
},
124+
},
125+
],
101126
},
102-
},
103-
canAcquireResult: {
104-
status: 1,
105-
extendedError: 0,
106-
},
107-
canLicenseStatusEnum: {
108-
Licensable: 1,
109-
},
127+
}),
128+
collectionQueryResult: createProductQueryResult({
129+
[HAGICODE_SPONSOR_PLAN_STORE_ID]: {
130+
storeId: HAGICODE_SPONSOR_PLAN_STORE_ID,
131+
title: 'Hagicode Sponsor Plan',
132+
isInUserCollection: true,
133+
skus: [
134+
{
135+
storeId: `${HAGICODE_SPONSOR_PLAN_STORE_ID}/0010`,
136+
title: 'Monthly subscription',
137+
isSubscription: true,
138+
isInUserCollection: true,
139+
collectionData: {
140+
endDate: toWinRtDateTime(expirationDate),
141+
},
142+
},
143+
],
144+
},
145+
}),
110146
});
111147

112148
assert.equal(state.availability, 'supported');
113149
assert.equal(state.product?.storeId, HAGICODE_SPONSOR_PLAN_STORE_ID);
114-
assert.equal(state.license?.isActive, true);
115-
assert.equal(state.license?.expirationDate, expirationDate);
116-
assert.equal(state.purchaseEligibility, 'licensable');
150+
assert.equal(state.product?.isInUserCollection, true);
151+
assert.equal(state.sku?.isInUserCollection, true);
152+
assert.equal(state.sku?.collectionEndDate, expirationDate);
153+
assert.equal(state.license, null);
154+
assert.equal(state.purchaseEligibility, 'license-action-not-applicable');
117155
assert.equal(state.errorCode, null);
118156
});
119157

120-
it('uses product-configured Store IDs when reading TurboEngine ownership', () => {
121-
const state = buildSupportedStateFromMinimalStoreApis({
158+
it('uses product-configured Store IDs when reading TurboEngine ownership from collection queries', () => {
159+
const state = buildSupportedStateFromProductQueries({
122160
fetchedAt: '2026-06-14T06:00:00.000Z',
123-
appLicense: {
124-
isActive: true,
125-
addOnLicenses: {
126-
hasKey: (key) => key === HAGICODE_TURBOENGINE_STORE_ID,
127-
get: (key) => (key === HAGICODE_TURBOENGINE_STORE_ID
128-
? {
129-
skuStoreId: HAGICODE_TURBOENGINE_STORE_ID,
130-
isActive: true,
131-
}
132-
: undefined),
161+
associatedQueryResult: createProductQueryResult({
162+
[HAGICODE_TURBOENGINE_STORE_ID]: {
163+
storeId: HAGICODE_TURBOENGINE_STORE_ID,
164+
title: 'TurboEngine',
165+
isInUserCollection: false,
166+
skus: [
167+
{
168+
storeId: `${HAGICODE_TURBOENGINE_STORE_ID}/0010`,
169+
title: 'TurboEngine durable',
170+
isSubscription: false,
171+
isInUserCollection: false,
172+
},
173+
],
133174
},
134-
},
135-
canAcquireResult: {
136-
status: 1,
137-
extendedError: 0,
138-
},
139-
canLicenseStatusEnum: {
140-
Licensable: 1,
141-
},
175+
}),
176+
collectionQueryResult: createProductQueryResult({
177+
[HAGICODE_TURBOENGINE_STORE_ID]: {
178+
storeId: HAGICODE_TURBOENGINE_STORE_ID,
179+
title: 'TurboEngine',
180+
isInUserCollection: true,
181+
skus: [
182+
{
183+
storeId: `${HAGICODE_TURBOENGINE_STORE_ID}/0010`,
184+
title: 'TurboEngine durable',
185+
isSubscription: false,
186+
isInUserCollection: true,
187+
},
188+
],
189+
},
190+
}),
142191
productConfig: turboEngineProductConfig,
143192
});
144193

145194
assert.equal(state.product?.storeId, HAGICODE_TURBOENGINE_STORE_ID);
146195
assert.equal(state.product?.title, 'TurboEngine');
147-
assert.equal(state.license?.isActive, true);
196+
assert.equal(state.product?.isInUserCollection, true);
197+
assert.equal(state.sku?.isInUserCollection, true);
198+
assert.equal(state.purchaseEligibility, 'license-action-not-applicable');
148199
});
200+
201+
it('routes reads and purchases through the packaged native addon without a dynwinrt JS fallback', async () => {
202+
const source = await fs.readFile(brokerPath, 'utf8');
203+
204+
assert.match(source, /executeWindowsStoreStatusAddon\(\{/);
205+
assert.match(source, /executeWindowsStorePurchaseAddon\(\{/);
206+
assert.doesNotMatch(source, /StoreContext\.getDefault/);
207+
assert.doesNotMatch(source, /@microsoft\/dynwinrt/);
208+
});
209+
149210
});

0 commit comments

Comments
 (0)