microsoft / windows-rs

Rust for Windows
https://kennykerr.ca/rust-getting-started/
Apache License 2.0
10.49k stars 497 forks source link

Possible to implement Microsoft Store purchase functionality? #1218

Closed ndarilek closed 3 years ago

ndarilek commented 3 years ago

I have a Rust game I'm trying to add to the Microsoft Store. Currently it seems to correctly determine when it is running as a free trial, but I'm having trouble figuring out how to implement the purchase flow.

The solution seems to be documented here: https://docs.microsoft.com/en-us/windows/uwp/monetize/in-app-purchases-and-trials#desktop Specifically:

  1. I can use IInitializeWithWindow in my app, but I'm not sure a) how to cast StoreContext to that and set the main window handle, or even if I can.
  2. I need the main window handle, but am not sure how to retrieve it from the Rust bindings. I searched for MainWindowHandle as per this line in the sample code:

initWindow.Initialize(System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle);

I know implementing COM interfaces isn't supported yet, but I'm not sure if that's what I need to pull this off. I'd really appreciate knowing whether or not this is even possible. :)

Thanks.

riverar commented 3 years ago

@ndarilek Hi there. Can you share some sample code so we can understand the context you're working in? Then we'll have a better idea of what's available to you. (e.g. Is this a pure win32 desktop app? uwp? etc.)

kennykerr commented 3 years ago

You can use the cast method to query for an interface.

I know implementing COM interfaces isn't supported yet

Support for implementing COM interfaces is now available.

ndarilek commented 3 years ago

This is a Bevy game, running in a window presumably created by WGPU.

I have this code which seems to correctly detect if my game is a trial or not. Ignore the bit about the demo feature--that is used to enable demo functionality outside the Microsoft Store, which gets the full non-demo version and should upgrade via IAP.

pub fn is_demo() -> bool {
    if cfg!(feature = "demo") {
        return true;
    }
    #[cfg(windows)]
    {
        if let Ok(context) = StoreContext::GetDefault() {
            if let Ok(license) = context.GetAppLicenseAsync() {
                if let Ok(license) = license.get() {
                    if let Ok(trial) = license.IsTrial() {
                        trial
                    } else {
                        false
                    }
                } else {
                    false
                }
            } else {
                false
            }
        } else {
            false
        }
    }
    #[cfg(not(windows))]
    false
}

Yeah, can certainly be optimized, but I kind of expected it not to work so wrote it verbosely. :)

Then this code makes the purchase when a button is clicked, but doesn't work:

pub fn purchase_upgrade() {
    #[cfg(windows)]
    {
        if let Ok(context) = StoreContext::GetDefault() {
            if let Err(e) = context.RequestPurchaseAsync(MICROSOFT_STORE_ID) {
                eprintln!("Error purchasing product: {:?}", e);
            }
        }
    }
}

But that code fails to run. Unfortunately I'm having a tough time debugging this because I essentially have to round-trip through approval and certification to test this in a store context, my last version used error!, and I'm speculating that perhaps error logging isn't set up in release builds. I'll see if this eprintln triggers instead. But for the time being I'm speculating that perhaps the window handle needs to be registered. I'm new enough to the win32 APIs to not be sure whether or not this failure case is relevant to me.

As an aside, I can apparently link locally built apps to their store installations and, after installing the store version once, debug issues like this in local builds. What should I google to figure out how to do that outside of a Visual Studio context? Is there a command line incantation to link a local appxmanifest/executable with the same version of that app installed through the store? I've searched, but don't even know what keywords to try and nothing has sent me anywhere useful.

Thanks.

kennykerr commented 3 years ago

Neat! I have been meaning to play with the Bevy game engine.

I don't have any experience working with the store - that seems hard to debug. 😨 Hopefully @riverar has some suggestions.

riverar commented 3 years ago

@ndarilek I just learned of Bevy and its Entity Component System (ECS) a few hours ago so this may be a little contrived and/or not idiomatic Bevy, but I think this will work for you. I strongly recommend opening an issue on Bevy asking them to expose their Window HWNDs for platform interoperability scenarios like this one. (They have access to it via winit.)

The example below

cargo.toml

...
[dependencies]
bevy = "0.5"
bevy_egui = "0.7"
windows = "0.21.1"

[build-dependencies]
windows = "0.21.1"

build.rs

fn main() {
    windows::build!({
        Windows::Services::Store::StoreContext,
        Windows::Win32::UI::Shell::IInitializeWithWindow,
        Windows::Win32::UI::KeyboardAndMouseInput::GetActiveWindow
    })
}

main.rs

#![windows_subsystem = "console"]

mod bindings {
    windows::include_bindings!();
}

use bindings::Windows::Services::Store::*;
use bindings::Windows::Win32::Foundation::HWND;
use bindings::Windows::Win32::UI::KeyboardAndMouseInput::GetActiveWindow;
use bindings::Windows::Win32::UI::Shell::IInitializeWithWindow;

use bevy::{
    prelude::*,
    window::WindowCreated
};
use bevy_egui::{
    egui::{self, Id},
    EguiContext, EguiPlugin,
};
use windows::Interface;

pub struct MainWindowHandle(HWND);

fn main() {
    App::build()
        .add_plugins(DefaultPlugins)
        .add_plugin(EguiPlugin)
        .insert_resource(MainWindowHandle(HWND::default()))
        .add_system(simple.system())
        .add_startup_system(register_main_window_handle.system())
        .run();
}

fn register_main_window_handle(
    mut events: EventReader<WindowCreated>,
    mut main_window_handle: ResMut<MainWindowHandle>,
) {
    if let Some(event) = events.iter().last() {
        main_window_handle.0 = unsafe { GetActiveWindow() };
    }
}

fn simple(egui_context: ResMut<EguiContext>, main_window_handle: Res<MainWindowHandle>) {
    egui::Window::new("Limited time offer!")
        .id(Id::new("iap_window"))
        .title_bar(true)
        .show(egui_context.ctx(), |ui| {
            if ui.button("Purchase now! (0.00 Ferris)").clicked() {
                let store_context = StoreContext::GetDefault().unwrap();
                let interop: IInitializeWithWindow = store_context.cast().unwrap();

                if unsafe { interop.Initialize(main_window_handle.0) }.is_ok() {
                    // Do something with store_context here
                    // e.g. GetStoreProductForCurrentAppAsync()
                }
            }
        });
}

image

riverar commented 3 years ago

I'll send an email internally to ask about the developer story for Windows.Services.Store.* APIs. Maybe there are some test hooks we can leverage? Pushing to the Store and waiting is frankly ridiculous.

ndarilek commented 3 years ago

Wow, thanks, that's way more than I expected. I'll see about packaging this into a Bevy plugin and publishing it so folks will have an easier time. I appreciate you looking into whether testing without round-tripping through the store is possible. Adding it to the existing documentation would also be immensely helpful. I've found command line oriented docs for other steps in this flow except for this one.

While I have your attention, can I hold onto the StoreContext and stash that in a resource as well, so systems aren't having to wait on a future every time they need it? I guess I'm wondering if methods like GetAppLicenseAsync get license state as it was when the context was initialized, or if they pick up changes? I know I can register an event handler, and I may do that too, but that's a bit more complicated than I need if these methods will pick up context changes as they're made. Maybe adding the license as a resource would work too.

Thanks again.

riverar commented 3 years ago

 can I hold onto the StoreContext and stash that in a resource as well, so systems aren't having to wait on a future every time they need it?

Ah the initialize dance in the simple system, yep stashing that should be fine and probably ideal.

ndarilek commented 3 years ago

Right, so you'd populate the resource in the startup system, and future systems could directly get a Res<StoreContext> or Res<License> without needing to go through this dance each time. I just wasn't sure if the retrieved context/license represented a single point in time, or if it updated state based on purchase status and such.

Thanks, this helps a lot. Traveling today but I'll give it a shot on Monday and see what happens. Hopefully we can figure out a way to test things out without having to wait on certification round trips for each test. :)

riverar commented 3 years ago

Here's a facility (WindowsStoreProxy.xml) to work offline https://docs.microsoft.com/en-us/windows/uwp/monetize/in-app-purchases-and-trials-using-the-windows-applicationmodel-store-namespace#get-started-with-the-currentapp-and-currentappsimulator-classes

Haven't tried it yet but looks promising.

riverar commented 3 years ago

Going to close this now as there's no work for the windows crate, but feel free to continue discussions. Just administrative overhead.

ndarilek commented 3 years ago

Thanks, I'm not sure that link is relevant. From the first section on that page:

Important The Windows.ApplicationModel.Store namespace is no longer being updated with new features. If your project targets Windows 10 Anniversary Edition (10.0; Build 14393) or a later release in Visual Studio (that is, you are targeting Windows 10, version 1607, or later), we recommend that you use the Windows.Services.Store namespace instead. For more information, see In-app purchases and trials. The Windows.ApplicationModel.Store namespace is not supported in Windows desktop applications that use the Desktop Bridge or in apps or games that use a development sandbox in Partner Center (for example, this is the case for any game that integrates with Xbox Live). These products must use the Windows.Services.Store namespace to implement in-app purchases and trials.

I'll see if I can get a build through certification tomorrow and test out your code. Thanks again for all the help!

riverar commented 3 years ago

@ndarilek Noticed that too, sent a few emails to get updated guidance. Will follow up if I hear back.

ndarilek commented 3 years ago

One more snag. I finally got my purchase flow to give me an error:

Error purchasing product: Error { code: 0x80070578, message: "This function must be called from a UI thread", win32_erro r: 1400 }

How do I run a closure or equivalent on the UI thread using these bindings? How does that work when there isn't an actual UI?

Thanks again.

ndarilek commented 3 years ago

Hitting a snag with 0.23.0:

[dependencies.windows]
version = "0.23"
features = [
    "Foundation",
    "Services_Store",
    "Win32_UI",
    "Win32_UI_KeyboardAndMouseInput",
    "Win32_UI_Shell",
]

error[E0432]: unresolved import windows::Win32::UI::KeyboardAndMouseInput::GetActiveWindow
--> src\iap.rs:18:17
|
18 | Win32::UI::{KeyboardAndMouseInput::GetActiveWindow, Shell::IInitializeWithWindow},
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ no GetActiveWindow in Win32::UI::KeyboardAndMouseInput

error[E0599]: no method named Initialize found for struct IInitializeWithWindow in the current scope
--> src\iap.rs:116:53
|
116 | ... if unsafe { interop.Initialize(GetActiveWindow()) }.is_ok() {
| ^^^^^^^^^^ method not found in IInitializeWithWindow

What features am I missing? Would be great if the docs included the needed feature for a given module/name. If they do then I'm missing it--at least in search results.

Thanks!

riverar commented 3 years ago

@ndarilek What you need for GetActiveWindow is ["Win32_Foundation", "Win32_UI_Input_KeyboardAndMouse" ]. The general workflow is:

  1. find the features tied to the method/type you're using
  2. find the features tied to the containing module

The pre-generated code lives at https://github.com/microsoft/windows-rs/tree/master/src/Windows

Bubbling up all the required features via docs is something def on the radar and being worked on as you read this.