@mpgd/cli 0.47.0 → 0.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/command-launcher.d.ts +13 -0
- package/dist/command-launcher.js +24 -0
- package/dist/devvit-output-collisions.d.ts +49 -0
- package/dist/devvit-output-collisions.js +120 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +186 -10
- package/dist/microsoft-store-submission.js +11 -6
- package/dist/native-build-info.json +2 -2
- package/dist/native-build-target.js +2134 -351
- package/dist/tooling-migration.d.ts +24 -0
- package/dist/tooling-migration.js +497 -0
- package/dist/windows-host/Core/BridgeCommand.cs +17 -0
- package/dist/windows-host/Core/Mpgd.MicrosoftStore.Host.csproj +9 -0
- package/dist/windows-host/Core/NativeBridgeContracts.cs +33 -0
- package/dist/windows-host/Core/NativeBridgeHost.cs +162 -0
- package/dist/windows-host/Core/NativeBridgeOrigin.cs +19 -0
- package/dist/windows-host/Core/NativeBridgeProtocol.cs +335 -0
- package/dist/windows-host/Core/NativeBridgeRequestGate.cs +76 -0
- package/dist/windows-host/Core/NativeBridgeResult.cs +65 -0
- package/dist/windows-host/README.md +118 -0
- package/dist/windows-host/WinUI/DispatcherQueueBridgeDispatcher.cs +39 -0
- package/dist/windows-host/WinUI/WebView2DocumentTransport.cs +46 -0
- package/dist/windows-host/WinUI/WebView2NativeBridge.cs +185 -0
- package/dist/windows-host/WinUI/WindowStoreContext.cs +20 -0
- package/dist/windows-native-tools.d.ts +4 -0
- package/dist/windows-native-tools.js +21 -0
- package/native-tools/windows-msix/WindowsMsixAcceptance.psm1 +333 -0
- package/package.json +30 -18
- package/templates/lite-overlay/.agents/skills/use-mpgd-kit/SKILL.md +22 -0
- package/templates/lite-overlay/AGENTS.md +32 -0
- package/templates/lite-overlay/README.md +37 -0
- package/templates/lite-overlay/agent/acceptance.md +27 -0
- package/templates/lite-overlay/agent/brief.md +17 -0
- package/templates/lite-overlay/apps/target-devvit/README.md +134 -0
- package/templates/lite-overlay/docs/MPGD_KIT_WORKFLOWS.md +37 -0
- package/templates/lite-overlay/src/lite/tapGame.ts +77 -0
- package/templates/lite-overlay/src/platform/devvitEntrypoint.ts +116 -0
- package/templates/lite-overlay/src/platform/devvitInlineMode.css +146 -0
- package/templates/lite-overlay/src/runtime/createGame.ts +57 -0
- package/templates/phaser-game/AGENTS.md +5 -0
- package/templates/phaser-game/README.md +5 -3
- package/templates/phaser-game/apps/target-ait/package.json +4 -4
- package/templates/phaser-game/apps/target-ait/src/main.ts +5 -5
- package/templates/phaser-game/apps/target-cloudflare-pages/package.json +2 -2
- package/templates/phaser-game/apps/target-devvit/package.json +2 -2
- package/templates/phaser-game/apps/target-devvit/src/server/bridge.ts +137 -18
- package/templates/phaser-game/apps/target-devvit/vite.config.ts +2 -0
- package/templates/phaser-game/package.json +8 -8
- package/templates/phaser-game/pnpm-workspace.yaml +2 -0
- package/templates/phaser-game/src/env.d.ts +2 -0
- package/templates/phaser-game/src/platform/buildGateways/ait.ts +1 -1
- package/templates/phaser-game/src/platform/buildGateways/aitSandbox.ts +9 -4
- package/templates/phaser-game/src/platform/buildGateways/microsoftStore.ts +2 -5
- package/templates/phaser-game/src/platform/buildGateways/reddit.ts +1 -1
- package/templates/phaser-game/src/platform/buildGateways/verse8.ts +1 -2
- package/templates/phaser-game/src/platform/verse8Agent8Client.ts +5 -6
- package/templates/phaser-game/tools/{run-game-acceptance.mjs → run-game-acceptance.ts} +1 -8
- package/templates/phaser-game/tsconfig.tools.json +12 -0
- package/templates/phaser-game/vite.shared.ts +40 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Microsoft Store native host support
|
|
2
|
+
|
|
3
|
+
These C# sources ship inside the existing `@mpgd/cli` npm package. No additional
|
|
4
|
+
npm or NuGet package is required. The core targets .NET 8 without SDK or NuGet
|
|
5
|
+
dependencies. The optional WinUI files compile inside your existing Windows
|
|
6
|
+
App SDK / WebView2 host.
|
|
7
|
+
|
|
8
|
+
```js
|
|
9
|
+
import { getMicrosoftStoreNativeHostSourceDirectory } from '@mpgd/cli/windows-native-tools';
|
|
10
|
+
const sources = getMicrosoftStoreNativeHostSourceDirectory();
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The locator returns the installed `dist/windows-host` directory. SDK-dependent
|
|
14
|
+
sources are maintained in `native-plugins/microsoft-store-host/windows` and copied
|
|
15
|
+
there during the CLI build. SDK-independent sources stay in the CLI source tree.
|
|
16
|
+
|
|
17
|
+
Copy the source tree into a game-owned, ignored build directory. Reference
|
|
18
|
+
`Core/Mpgd.MicrosoftStore.Host.csproj` or link `Core/*.cs` directly; link
|
|
19
|
+
`WinUI/*.cs` only into a Windows host. Keep `bin` and `obj` outside the installed
|
|
20
|
+
npm package. CI builds and exercises these files after packing and installing
|
|
21
|
+
the CLI, without relying on a workspace project reference.
|
|
22
|
+
|
|
23
|
+
## Attach to a window
|
|
24
|
+
|
|
25
|
+
On the window's UI thread, initialize WebView2, then attach before navigating:
|
|
26
|
+
|
|
27
|
+
```csharp
|
|
28
|
+
await webView.EnsureCoreWebView2Async();
|
|
29
|
+
StoreContext store = WindowStoreContext.Create(window);
|
|
30
|
+
var bridge = await WebView2NativeBridge.AttachAsync(
|
|
31
|
+
webView, new Uri(gameConfiguration.StartUri),
|
|
32
|
+
gameConfiguration.AllowedItemIds, gameOwnedOperations);
|
|
33
|
+
window.Closed += (_, _) => bridge.Dispose();
|
|
34
|
+
webView.CoreWebView2.Navigate(gameConfiguration.StartUri);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The adapter checks HTTPS host and port, rejects credentials in navigation URLs,
|
|
38
|
+
blocks new windows, disables host objects and devtools, and accepts messages
|
|
39
|
+
from a committed, allowed document as it starts loading, including startup
|
|
40
|
+
requests before the load-complete event. Failed navigation and every accepted navigation
|
|
41
|
+
start invalidates the document. Blocked external navigation keeps the intact
|
|
42
|
+
page's bridge active. Browser or main renderer exit invalidates it; auto-recovered
|
|
43
|
+
GPU, utility and subframe failures and renderer unresponsiveness preserve it.
|
|
44
|
+
Queued operations are cancelled and late replies from the previous document are
|
|
45
|
+
suppressed, including navigation
|
|
46
|
+
back to the same URL. Closure detaches handlers and suppresses responses. Use
|
|
47
|
+
one adapter and operation implementation per window.
|
|
48
|
+
|
|
49
|
+
Await `AttachAsync` before navigating. It installs a document-created script
|
|
50
|
+
that wraps only the native v1 requests with a random per-document nonce; the
|
|
51
|
+
public TypeScript client and v1 payload remain unchanged. Incoming messages must
|
|
52
|
+
match the live document's nonce after a navigation recheck, so queued messages
|
|
53
|
+
from an old page cannot start checkout after a same-origin reload. Other WebView2
|
|
54
|
+
message protocols are preserved. Disposing removes the script registration.
|
|
55
|
+
|
|
56
|
+
`WindowStoreContext.Create` initializes the SDK with that window's HWND on its
|
|
57
|
+
UI thread. Keep the context in native code. Initialization does not perform
|
|
58
|
+
login, checkout or grant an entitlement.
|
|
59
|
+
|
|
60
|
+
## Game-owned operations
|
|
61
|
+
|
|
62
|
+
Implement `INativeStoreOperations` with typed callbacks for catalog, sign-in,
|
|
63
|
+
purchase listing and checkout. Callbacks begin on the window UI thread and are
|
|
64
|
+
serialized. Observe cancellation whenever the SDK permits it. An SDK operation
|
|
65
|
+
that ignores cancellation retains its serialization lease until it finishes;
|
|
66
|
+
its result is still suppressed after navigation or closure.
|
|
67
|
+
|
|
68
|
+
The common code parses `mpgd.microsoft-store.native.v1`, enforces message limits,
|
|
69
|
+
allowed products, duplicate IDs and pending-request limits, and serializes only
|
|
70
|
+
the declared bounded result records. Invalid results and unexpected exceptions
|
|
71
|
+
become `NATIVE_OPERATION_FAILED`; exception messages never reach the web page.
|
|
72
|
+
Use `NativeBridgeOperationException` for declared user-cancelled,
|
|
73
|
+
identity-unavailable or store-unavailable errors.
|
|
74
|
+
User cancellation of a purchase callback produces a `not-purchased` result,
|
|
75
|
+
matching the existing web commerce runtime's abort semantics.
|
|
76
|
+
|
|
77
|
+
The game owns Store IDs and offers, MSAL/WAM account selection, backend endpoints,
|
|
78
|
+
SDK product validation and session issuance. Sign-in returns the game backend's
|
|
79
|
+
short-lived player session, never an SDK access token. Collections IDs, Microsoft
|
|
80
|
+
service tickets and account credentials stay in private native/backend state.
|
|
81
|
+
Client checkout produces evidence; the backend ledger verifies and commits all
|
|
82
|
+
durable grants.
|
|
83
|
+
|
|
84
|
+
`NativePlayerSession.ExpiresAt` uses Unix epoch milliseconds, with 2 minutes to
|
|
85
|
+
24 hours remaining at serialization, leaving a minute of transport headroom
|
|
86
|
+
above the web client's 1-minute acceptance minimum. It accepts a bounded player ID
|
|
87
|
+
and a whitespace-free token of 32–4096 characters. Games may enforce a lower
|
|
88
|
+
maximum lifetime. Product and purchase results must refer to their allowlist. Purchase
|
|
89
|
+
tokens equal their item ID, matching the existing web client contract; the
|
|
90
|
+
authoritative backend still verifies and commits the purchase.
|
|
91
|
+
|
|
92
|
+
## Core-only integration
|
|
93
|
+
|
|
94
|
+
For a custom transport, construct `NativeBridgeHost` with an
|
|
95
|
+
`INativeBridgeDispatcher`. Run queued callbacks on the owning UI thread, prevent
|
|
96
|
+
cancelled queued callbacks from starting, and await started operations until
|
|
97
|
+
they finish. `postResponse` synchronously sends JSON on that thread. Call
|
|
98
|
+
`InvalidateDocument` at every accepted navigation start, `ActivateDocument` only after an
|
|
99
|
+
allowed document commits, invalidate again on load failure, and `Dispose` at
|
|
100
|
+
closure. Invalidate on process failure only when the top-level document exits;
|
|
101
|
+
auto-recovered helper/subframe failures retain it. Lifecycle calls
|
|
102
|
+
and response delivery must share the window UI dispatcher.
|
|
103
|
+
Authenticate each incoming message's document generation before calling the
|
|
104
|
+
host; a source URI alone cannot distinguish queued messages after a same-origin
|
|
105
|
+
reload. The WinUI adapter supplies this transport check automatically.
|
|
106
|
+
|
|
107
|
+
## Acceptance scope
|
|
108
|
+
|
|
109
|
+
Contract tests cover two independent game configurations, protocol and product
|
|
110
|
+
rejection, sanitized errors, duplicate/busy requests, cancelled queues, navigation
|
|
111
|
+
generation and closure. Windows CI links the installed package's core and WinUI
|
|
112
|
+
sources into x64/ARM64 MSIX, binds StoreContext to the native window, and exercises
|
|
113
|
+
four methods through WebView2 with fixture callbacks. It also checks requests
|
|
114
|
+
before load completion, real blocked external navigation, and simulated
|
|
115
|
+
recoverable process notification kinds on the live adapter. It does not terminate
|
|
116
|
+
GPU processes. Receipts identify fixture
|
|
117
|
+
results separately. They do not claim real Store login, settled purchases,
|
|
118
|
+
backend grants or Store upload.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
using Microsoft.UI.Dispatching;
|
|
2
|
+
|
|
3
|
+
namespace Mpgd.MicrosoftStore.Host;
|
|
4
|
+
|
|
5
|
+
public sealed class DispatcherQueueBridgeDispatcher(DispatcherQueue queue) : INativeBridgeDispatcher
|
|
6
|
+
{
|
|
7
|
+
public async Task RunAsync(Func<Task> callback, CancellationToken cancellation)
|
|
8
|
+
{
|
|
9
|
+
ArgumentNullException.ThrowIfNull(callback);
|
|
10
|
+
cancellation.ThrowIfCancellationRequested();
|
|
11
|
+
if (queue.HasThreadAccess)
|
|
12
|
+
{
|
|
13
|
+
await callback();
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
17
|
+
int state = 0; // queued, running, cancelled-before-start
|
|
18
|
+
using var registration = cancellation.Register(() =>
|
|
19
|
+
{
|
|
20
|
+
if (Interlocked.CompareExchange(ref state, 2, 0) == 0)
|
|
21
|
+
completion.TrySetCanceled(cancellation);
|
|
22
|
+
});
|
|
23
|
+
if (!queue.TryEnqueue(async () =>
|
|
24
|
+
{
|
|
25
|
+
if (Interlocked.CompareExchange(ref state, 1, 0) != 0) return;
|
|
26
|
+
try
|
|
27
|
+
{
|
|
28
|
+
cancellation.ThrowIfCancellationRequested();
|
|
29
|
+
await callback();
|
|
30
|
+
completion.TrySetResult();
|
|
31
|
+
}
|
|
32
|
+
catch (OperationCanceledException error) { completion.TrySetCanceled(error.CancellationToken); }
|
|
33
|
+
catch (Exception error) { completion.TrySetException(error); }
|
|
34
|
+
})) completion.TrySetException(new InvalidOperationException("The window dispatcher is unavailable."));
|
|
35
|
+
// After starting, wait for the native operation to finish even if it
|
|
36
|
+
// ignores cancellation; the host's serialization lease must stay held.
|
|
37
|
+
await completion.Task.ConfigureAwait(false);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
using System.Text.Json;
|
|
2
|
+
|
|
3
|
+
namespace Mpgd.MicrosoftStore.Host;
|
|
4
|
+
|
|
5
|
+
// The public v1 request stays unchanged inside a document-scoped envelope.
|
|
6
|
+
internal static class WebView2DocumentTransport
|
|
7
|
+
{
|
|
8
|
+
internal const string Protocol = "mpgd.microsoft-store.document.v1";
|
|
9
|
+
internal const string ReadNonceScript = "globalThis.__mpgdMicrosoftStoreDocumentNonce ?? null";
|
|
10
|
+
internal const string Bootstrap = """
|
|
11
|
+
(() => {
|
|
12
|
+
const view = globalThis.chrome?.webview;
|
|
13
|
+
if (!view) return;
|
|
14
|
+
const nonce = Array.from(crypto.getRandomValues(new Uint8Array(16)),
|
|
15
|
+
byte => byte.toString(16).padStart(2, '0')).join('');
|
|
16
|
+
const post = view.postMessage.bind(view);
|
|
17
|
+
Object.defineProperty(globalThis, '__mpgdMicrosoftStoreDocumentNonce', { value: nonce });
|
|
18
|
+
Object.defineProperty(view, 'postMessage', {
|
|
19
|
+
writable: false, configurable: false,
|
|
20
|
+
value: message => post(message?.protocol === 'mpgd.microsoft-store.native.v1'
|
|
21
|
+
? { transport: 'mpgd.microsoft-store.document.v1', documentNonce: nonce, request: message }
|
|
22
|
+
: message),
|
|
23
|
+
});
|
|
24
|
+
})();
|
|
25
|
+
""";
|
|
26
|
+
|
|
27
|
+
internal static bool TryReadRequest(string json, out string nonce, out string request)
|
|
28
|
+
{
|
|
29
|
+
nonce = request = "";
|
|
30
|
+
if (string.IsNullOrEmpty(json) || json.Length > NativeBridgeProtocol.MaximumMessageLength + 512) return false;
|
|
31
|
+
try
|
|
32
|
+
{
|
|
33
|
+
using var document = JsonDocument.Parse(json, new JsonDocumentOptions { MaxDepth = 10 });
|
|
34
|
+
var root = document.RootElement;
|
|
35
|
+
if (root.ValueKind != JsonValueKind.Object || root.EnumerateObject().Count() != 3
|
|
36
|
+
|| !root.TryGetProperty("transport", out var transport) || transport.ValueKind != JsonValueKind.String || transport.GetString() != Protocol
|
|
37
|
+
|| !root.TryGetProperty("documentNonce", out var token) || token.ValueKind != JsonValueKind.String
|
|
38
|
+
|| !Guid.TryParseExact(token.GetString(), "N", out _)
|
|
39
|
+
|| !root.TryGetProperty("request", out var payload) || payload.ValueKind != JsonValueKind.Object) return false;
|
|
40
|
+
nonce = token.GetString()!;
|
|
41
|
+
request = payload.GetRawText();
|
|
42
|
+
return request.Length <= NativeBridgeProtocol.MaximumMessageLength;
|
|
43
|
+
}
|
|
44
|
+
catch (JsonException) { return false; }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
using Microsoft.UI.Xaml.Controls;
|
|
2
|
+
using Microsoft.Web.WebView2.Core;
|
|
3
|
+
using System.Text.Json;
|
|
4
|
+
|
|
5
|
+
namespace Mpgd.MicrosoftStore.Host;
|
|
6
|
+
|
|
7
|
+
/// <summary>Attach after EnsureCoreWebView2Async, before the first navigation.
|
|
8
|
+
/// Dispose on Window.Closed. Host objects and devtools are disabled.</summary>
|
|
9
|
+
public sealed class WebView2NativeBridge : IDisposable
|
|
10
|
+
{
|
|
11
|
+
private readonly WebView2 view;
|
|
12
|
+
private readonly CoreWebView2 core;
|
|
13
|
+
private readonly NativeBridgeOrigin origin;
|
|
14
|
+
private readonly NativeBridgeHost host;
|
|
15
|
+
private readonly Action<Exception>? onTransportError;
|
|
16
|
+
private ulong navigationId;
|
|
17
|
+
private bool documentActivated;
|
|
18
|
+
private bool documentRejected;
|
|
19
|
+
private bool navigationRejected;
|
|
20
|
+
private bool navigationCommitted;
|
|
21
|
+
private string? lastCommittedSource;
|
|
22
|
+
private bool closed;
|
|
23
|
+
private string? bootstrapScriptId;
|
|
24
|
+
private int pendingDocumentChecks;
|
|
25
|
+
|
|
26
|
+
private WebView2NativeBridge(WebView2 view, Uri startUri, IEnumerable<string> allowedItemIds,
|
|
27
|
+
INativeStoreOperations operations, Action<Exception>? onTransportError = null)
|
|
28
|
+
{
|
|
29
|
+
ArgumentNullException.ThrowIfNull(view);
|
|
30
|
+
if (!view.DispatcherQueue.HasThreadAccess)
|
|
31
|
+
throw new InvalidOperationException("Attach the bridge on the window's UI thread.");
|
|
32
|
+
this.view = view;
|
|
33
|
+
core = view.CoreWebView2 ?? throw new InvalidOperationException("Initialize WebView2 before attaching the bridge.");
|
|
34
|
+
origin = new(startUri);
|
|
35
|
+
this.onTransportError = onTransportError;
|
|
36
|
+
host = new(origin, allowedItemIds, operations, new DispatcherQueueBridgeDispatcher(view.DispatcherQueue),
|
|
37
|
+
response => core.PostWebMessageAsJson(response));
|
|
38
|
+
core.Settings.AreDevToolsEnabled = false;
|
|
39
|
+
core.Settings.AreHostObjectsAllowed = false;
|
|
40
|
+
core.NavigationStarting += NavigationStarting;
|
|
41
|
+
core.ContentLoading += ContentLoading;
|
|
42
|
+
core.NavigationCompleted += NavigationCompleted;
|
|
43
|
+
core.NewWindowRequested += NewWindowRequested;
|
|
44
|
+
core.WebMessageReceived += WebMessageReceived;
|
|
45
|
+
core.ProcessFailed += ProcessFailed;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public static async Task<WebView2NativeBridge> AttachAsync(WebView2 view, Uri startUri,
|
|
49
|
+
IEnumerable<string> allowedItemIds, INativeStoreOperations operations, Action<Exception>? onTransportError = null)
|
|
50
|
+
{
|
|
51
|
+
var bridge = new WebView2NativeBridge(view, startUri, allowedItemIds, operations, onTransportError);
|
|
52
|
+
try
|
|
53
|
+
{
|
|
54
|
+
// Register before the first navigation so even boot-time requests
|
|
55
|
+
// carry the nonce of the document that actually posted them.
|
|
56
|
+
bridge.bootstrapScriptId = await bridge.core.AddScriptToExecuteOnDocumentCreatedAsync(WebView2DocumentTransport.Bootstrap);
|
|
57
|
+
return bridge;
|
|
58
|
+
}
|
|
59
|
+
catch { bridge.Dispose(); throw; }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private void NavigationStarting(object? sender, CoreWebView2NavigationStartingEventArgs args)
|
|
63
|
+
{
|
|
64
|
+
if (!origin.Allows(args.Uri))
|
|
65
|
+
{
|
|
66
|
+
args.Cancel = true;
|
|
67
|
+
// A cancelled navigation keeps the previous document. A blocked
|
|
68
|
+
// redirect can follow an allowed start that already revoked it.
|
|
69
|
+
if (args.NavigationId == navigationId)
|
|
70
|
+
{
|
|
71
|
+
navigationRejected = true;
|
|
72
|
+
if (!documentActivated && lastCommittedSource is not null)
|
|
73
|
+
documentActivated = host.ActivateDocument(lastCommittedSource);
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
navigationId = args.NavigationId;
|
|
78
|
+
documentActivated = false;
|
|
79
|
+
documentRejected = false;
|
|
80
|
+
navigationRejected = false;
|
|
81
|
+
navigationCommitted = false;
|
|
82
|
+
host.InvalidateDocument();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private void ContentLoading(object? sender, CoreWebView2ContentLoadingEventArgs args)
|
|
86
|
+
{
|
|
87
|
+
if (closed || args.NavigationId != navigationId || navigationRejected) return;
|
|
88
|
+
navigationCommitted = true;
|
|
89
|
+
documentRejected = args.IsErrorPage;
|
|
90
|
+
// The new document is committed before its startup scripts can send
|
|
91
|
+
// messages. Waiting for NavigationCompleted drops valid boot requests.
|
|
92
|
+
if (!documentRejected) documentActivated = host.ActivateDocument(core.Source);
|
|
93
|
+
else
|
|
94
|
+
{
|
|
95
|
+
documentActivated = false;
|
|
96
|
+
host.InvalidateDocument();
|
|
97
|
+
}
|
|
98
|
+
lastCommittedSource = documentActivated ? core.Source : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private void NavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs args)
|
|
102
|
+
{
|
|
103
|
+
if (closed || args.NavigationId != navigationId || navigationRejected) return;
|
|
104
|
+
if (!args.IsSuccess)
|
|
105
|
+
{
|
|
106
|
+
if (args.WebErrorStatus == CoreWebView2WebErrorStatus.OperationCanceled && documentActivated && !documentRejected)
|
|
107
|
+
return; // The committed page remains usable after a cancelled load.
|
|
108
|
+
if (!navigationCommitted && lastCommittedSource is not null)
|
|
109
|
+
{
|
|
110
|
+
documentActivated = host.ActivateDocument(lastCommittedSource);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
documentActivated = false;
|
|
114
|
+
lastCommittedSource = null;
|
|
115
|
+
host.InvalidateDocument();
|
|
116
|
+
}
|
|
117
|
+
else if (!documentActivated && !documentRejected)
|
|
118
|
+
{
|
|
119
|
+
documentActivated = host.ActivateDocument(core.Source);
|
|
120
|
+
lastCommittedSource = documentActivated ? core.Source : null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private static void NewWindowRequested(object? sender, CoreWebView2NewWindowRequestedEventArgs args) => args.Handled = true;
|
|
125
|
+
|
|
126
|
+
private void ProcessFailed(object? sender, CoreWebView2ProcessFailedEventArgs args) =>
|
|
127
|
+
HandleProcessFailure(args.ProcessFailedKind);
|
|
128
|
+
|
|
129
|
+
internal void HandleProcessFailure(CoreWebView2ProcessFailedKind kind)
|
|
130
|
+
{
|
|
131
|
+
// GPU/utility/subframe failures and an unresponsive renderer leave the
|
|
132
|
+
// top-level document intact or recover without a new navigation.
|
|
133
|
+
if (closed || kind is not (CoreWebView2ProcessFailedKind.BrowserProcessExited
|
|
134
|
+
or CoreWebView2ProcessFailedKind.RenderProcessExited)) return;
|
|
135
|
+
documentActivated = false;
|
|
136
|
+
lastCommittedSource = null;
|
|
137
|
+
host.InvalidateDocument();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private async void WebMessageReceived(object? sender, CoreWebView2WebMessageReceivedEventArgs args)
|
|
141
|
+
{
|
|
142
|
+
if (closed) return;
|
|
143
|
+
try { await HandleTransportMessageAsync(args.Source, args.WebMessageAsJson); }
|
|
144
|
+
catch (Exception error) { onTransportError?.Invoke(error); }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
internal async Task HandleTransportMessageAsync(string source, string json)
|
|
148
|
+
{
|
|
149
|
+
if (closed || !documentActivated || !origin.Allows(source) || pendingDocumentChecks >= 64
|
|
150
|
+
|| !WebView2DocumentTransport.TryReadRequest(json, out var nonce, out var request)) return;
|
|
151
|
+
ulong currentNavigation = navigationId;
|
|
152
|
+
pendingDocumentChecks++;
|
|
153
|
+
try
|
|
154
|
+
{
|
|
155
|
+
// WebMessageReceived can arrive after a same-origin reload commits.
|
|
156
|
+
// Read the live document, then recheck navigation before entering
|
|
157
|
+
// the host. Origin alone cannot authenticate a queued old message.
|
|
158
|
+
string liveNonce = await core.ExecuteScriptAsync(WebView2DocumentTransport.ReadNonceScript);
|
|
159
|
+
if (closed || currentNavigation != navigationId || !documentActivated
|
|
160
|
+
|| JsonSerializer.Deserialize<string>(liveNonce) != nonce) return;
|
|
161
|
+
await host.HandleMessageAsync(source, request);
|
|
162
|
+
}
|
|
163
|
+
finally { pendingDocumentChecks--; }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
public void Dispose()
|
|
167
|
+
{
|
|
168
|
+
if (!view.DispatcherQueue.HasThreadAccess)
|
|
169
|
+
throw new InvalidOperationException("Detach the bridge on the window's UI thread.");
|
|
170
|
+
if (closed) return;
|
|
171
|
+
closed = true;
|
|
172
|
+
host.Dispose();
|
|
173
|
+
core.NavigationStarting -= NavigationStarting;
|
|
174
|
+
core.ContentLoading -= ContentLoading;
|
|
175
|
+
core.NavigationCompleted -= NavigationCompleted;
|
|
176
|
+
core.NewWindowRequested -= NewWindowRequested;
|
|
177
|
+
core.WebMessageReceived -= WebMessageReceived;
|
|
178
|
+
core.ProcessFailed -= ProcessFailed;
|
|
179
|
+
if (bootstrapScriptId is not null)
|
|
180
|
+
{
|
|
181
|
+
try { core.RemoveScriptToExecuteOnDocumentCreated(bootstrapScriptId); }
|
|
182
|
+
catch (Exception error) when (error is InvalidOperationException or System.Runtime.InteropServices.COMException) { }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
using Microsoft.UI.Xaml;
|
|
2
|
+
using Windows.Services.Store;
|
|
3
|
+
|
|
4
|
+
namespace Mpgd.MicrosoftStore.Host;
|
|
5
|
+
|
|
6
|
+
/// <summary>Call on the owning window's UI thread before starting Store UI.</summary>
|
|
7
|
+
public static class WindowStoreContext
|
|
8
|
+
{
|
|
9
|
+
public static StoreContext Create(Window window)
|
|
10
|
+
{
|
|
11
|
+
ArgumentNullException.ThrowIfNull(window);
|
|
12
|
+
if (!window.DispatcherQueue.HasThreadAccess)
|
|
13
|
+
throw new InvalidOperationException("StoreContext must be initialized on its window's UI thread.");
|
|
14
|
+
var handle = WinRT.Interop.WindowNative.GetWindowHandle(window);
|
|
15
|
+
if (handle == IntPtr.Zero) throw new InvalidOperationException("The native window is not initialized.");
|
|
16
|
+
var context = StoreContext.GetDefault();
|
|
17
|
+
WinRT.Interop.InitializeWithWindow.Initialize(context, handle);
|
|
18
|
+
return context;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Locate the PowerShell 5.1 MSIX acceptance module shipped with this CLI. */
|
|
2
|
+
export declare function getMicrosoftStoreNativeAcceptanceModulePath(): string;
|
|
3
|
+
/** Locate the C# core and WinUI integration sources shipped with this CLI. */
|
|
4
|
+
export declare function getMicrosoftStoreNativeHostSourceDirectory(): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
/** Locate the PowerShell 5.1 MSIX acceptance module shipped with this CLI. */
|
|
5
|
+
export function getMicrosoftStoreNativeAcceptanceModulePath() {
|
|
6
|
+
const modulePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../native-tools/windows-msix/WindowsMsixAcceptance.psm1');
|
|
7
|
+
if (!statSync(modulePath).isFile()) {
|
|
8
|
+
throw new Error('The installed CLI is missing its Windows MSIX acceptance module.');
|
|
9
|
+
}
|
|
10
|
+
return modulePath;
|
|
11
|
+
}
|
|
12
|
+
/** Locate the C# core and WinUI integration sources shipped with this CLI. */
|
|
13
|
+
export function getMicrosoftStoreNativeHostSourceDirectory() {
|
|
14
|
+
const sourceDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), './windows-host');
|
|
15
|
+
for (const file of ['Core/Mpgd.MicrosoftStore.Host.csproj', 'WinUI/WebView2NativeBridge.cs']) {
|
|
16
|
+
if (!statSync(path.join(sourceDirectory, file)).isFile()) {
|
|
17
|
+
throw new Error('The installed CLI is missing its Microsoft Store native host sources.');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return sourceDirectory;
|
|
21
|
+
}
|