Skip to content

feat(microsoft-store): Microsoft Store purchases, app license, trial and updates - #891

Merged
kdroidFilter merged 11 commits into
nucleus-3.0from
feat/in-app-purchase-windows
Oct 31, 2026
Merged

kdroidFilter merged 11 commits into
nucleus-3.0from
feat/in-app-purchase-windows

Conversation

@kdroidFilter

@kdroidFilter kdroidFilter commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #566 — Windows only. Draft until the last Store checks below are done.

What

New runtime module nucleus.microsoft-store, backed by Windows.Services.Store (Rust, forked from tauri-plugin-iap, MIT, notice in THIRD_PARTY_NOTICES.md §8). One entry point, MicrosoftStore:

  • MicrosoftStore.purchases
    • Add-ons: products, purchase, list / restore, entitlement, consume (crash-safe, idempotent fulfilment), purchaseUpdates (renewals, refunds, purchases made elsewhere).
    • App license: appLicense() (active / trial / time left / SKU), purchaseApp() to convert a trial, appLicenseUpdates.
    • An owned durable, subscription or full app returns Success(alreadyOwned = true) without showing the Store, which was seen offering them for sale again.
  • MicrosoftStore.updates: check(), install() (progress flow, dialog or silent, registers the app for restart so Windows relaunches it), openStorePage().
  • macOS and Linux answer Unsupported.

Also in this PR: the GraalVM Windows image now ships jawt.dll next to the exe (javaaccessbridge.dll imports it; without it a Store-installed native image crashed at launch), and the Lab gets system.purchases and system.store-updates probes and an MSIX identity for its Store listing.

Notable

  • The MTA is pinned (CoIncrementMTAUsage), Rust workers attach to the JVM with a scoped guard, and the license listener is registered from an MTA thread (the Tao UI thread is an STA). Details in docs/internals/microsoft-store.md.
  • An install the Store did not license reads as an active, non-trial license with no SKU: AppLicense.isFullLicense requires a skuStoreId.
  • TrialTimeRemaining only applies to usage-limited trials; a time-limited trial's time left comes from ExpirationDate.

Verification

  • ./gradlew :microsoft-store:check, including WindowsNativeTortureTest on the real DLL and Store API.
  • On the real Store listing (Nucleus Lab, GraalVM MSIX): trial with time left, buy full version, add-on listing, durable purchase + entitlement, cancelled dialog, license events, in-app update 1.0.4 → 1.0.5.
  • Not yet verified on the Store: relaunch after an in-app update (1.0.6 → 1.0.7), the alreadyOwned guard, consumables, subscription purchase, refunds.

Windows backend over Windows.Services.Store, forked from tauri-plugin-iap
and decoupled from Tauri: products, purchase, consumables, subscriptions,
license change updates. macOS is not supported yet and Linux has no store;
both answer Unsupported from Kotlin, with no native library.
InAppPurchase.appLicense() reads StoreAppLicense (active, trial, time left,
trial unique id, SKU), purchaseApp() buys the app itself to convert a trial,
and appLicenseUpdates reports license state changes from
OfflineLicensesChanged. An install the Store did not license reads as an
active non-trial license without a SKU, so isFullLicense also requires one.
… Microsoft Store

system.purchases drives in-app-purchase on Windows: app license and trial,
full-version purchase, add-ons, consumables, purchases and the updates the
Store sends unasked. The Lab's AppX identity moves to its own reservation,
KDroidFilter.NucleusLab, which the probe is verified against.
javaaccessbridge.dll imports jawt.dll, which Windows resolves from the
executable's directory, but the plugin only copied it to bin/ for Skiko.
With Java Access Bridge enabled (screen readers, jabswitch -enable) AWT
failed at startup with UnsatisfiedLinkError: javaaccessbridge, unless a
JDK on PATH happened to provide a jawt.dll of its own. Seen on the Nucleus
Lab installed from the Microsoft Store.
Verified on the Nucleus Lab installed from the Microsoft Store (7-day
trial): TrialTimeRemaining reports the unlimited sentinel for a
time-limited trial, so trialTimeRemaining read null and the trial looked
unlimited. It is now derived from ExpirationDate whenever there is one.
The same install shows TrialUniqueId empty and IsTrialOwnedByThisUser
false: both only describe usage-limited trials, which the KDoc now says.
It also confirms a Store install carries a SkuStoreId (9MV8ZRKS4KT4/0011).
On the Nucleus Lab installed from the Microsoft Store, converting the
trial to a full license never produced an app license update. The
OfflineLicensesChanged subscription was made on the thread that first
collected an update flow, the UI thread, an STA (tao's OleInitialize),
where RoInitialize(MULTITHREADED) fails and the StoreContext stayed in
that apartment. It is now made from a dedicated MTA thread, and a Rust
test subscribes from an STA and checks the apartment.

Every license notification is also logged at FINE, and the Lab probe
lists those lines, so a real install shows whether notifications arrive.
StoreUpdates checks the Store for a newer version of the app
(GetAppAndOptionalStorePackageUpdatesAsync: version and mandatory flag),
installs it from inside the app with the Store's dialog or silently,
streaming StorePackageUpdateStatus progress as a Flow, and opens the
app's Store page. It shares the purchase bridge: progress travels as
updateProgress events routed to the request that asked, and cancelling
the collection cancels the download. New StoreErrorCode.UpdateBlocked for
an update the Store holds until charging or Wi-Fi.

The Lab gets a Store updates probe to verify it on the real listing.
On the Nucleus Lab installed from the Microsoft Store, the purchase
dialog offered an owned durable add-on, and the owned app itself (full
license, SKU 0010), for sale again, asking to pay a second time.
Microsoft documents that a non-expiring durable can only be purchased
once and recommends checking the license before offering a purchase; no
other library checks first. purchase() (non-consumables and
subscriptions) and purchaseApp() now read this device's licenses before
any window and answer Success(alreadyOwned = true). A device whose
licenses have not synced yet still gets the Store's dialog.
The first in-app update of the Nucleus Lab from the Microsoft Store
(1.0.4 -> 1.0.5) installed, closed the app and left it closed. Microsoft
documents the relaunch of a desktop app the Store updates as opt-in
through RegisterApplicationRestart. StoreUpdates.install now registers
the app right before installing, for updates only (no crash, hang or
reboot restarts), and unregisters when the install returns without
replacing the process. Windows skips it for an app that ran under 60 s.

The KDoc no longer claims Windows relaunches the app on its own.
The module covers more than purchases now (app license, trial, Store updates)
and only ever targets the Microsoft Store, so it is named after it:

- module microsoft-store, artifact nucleus.microsoft-store
- package dev.nucleusframework.store.microsoft, native lib nucleus_msstore
- one root object: MicrosoftStore.purchases (list/restore replace
  purchases/restorePurchases) and MicrosoftStore.updates
@kdroidFilter kdroidFilter changed the title feat(in-app-purchase): Microsoft Store in-app purchases, app license and trial feat(microsoft-store): Microsoft Store purchases, app license, trial and updates Oct 31, 2026
@kdroidFilter
kdroidFilter marked this pull request as ready for review October 31, 2026 21:23
@kdroidFilter
kdroidFilter merged commit e7f9898 into nucleus-3.0 Oct 31, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant