Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
279 changes: 57 additions & 222 deletions AGENTS.md

Large diffs are not rendered by default.

32 changes: 23 additions & 9 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ All notable changes to this package will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.1.0] - 2026-08-13

**New**:
- Added non-dismissible alerts for blocking application flows and programmatic alert dismissal without invoking a button callback.
- Runtime alert calls now render an interactive platform-shaped mock in the Editor Game view without requiring the Device Simulator window.

**Changed**:
- Declared Unity 6000.0 as the package minimum and documented 6000.0.x, 6000.3.x, and 6000.5.x as compatibility reference streams.
- Moved the keep-awake convenience API to the static `DeviceService.KeepAwake` property and removed the redundant `IScreenWakeService` / `ScreenWakeService` child service.
- Alerts now require one to three buttons with unique labels and styles so iOS and Android resolve the same action.

**Fixed**:
- Android alerts are created and shown on the Android UI thread, retain callback proxies until dismissal, and tolerate buttons with no callback.

## [1.0.1] - 2026-08-12

**Fixed**:
Expand All @@ -29,17 +43,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Device Simulator Plugin**: An embedded Unity Device Simulator panel provides platform-shaped native UI mocks, live diagnostics, and a per-preset haptic envelope graph.
- **Mobile Services Config asset**: Configure localized permission descriptions, capability toggles, Android manifest opt-ins, and Play In-App Review Gradle setup from `Tools > GameLovers > Mobile Services > Select Mobile Services Config`.
- **Build Postprocessor**: Automatically inject iOS usage descriptions and entitlements, Android manifest entries, and the Play In-App Review Gradle dependency, with validation for missing configuration.
- **Samples**: Added one importable **Mobile Services Samples** bundle containing independently playable Playground, Haptics Palette, Notifications Scheduler, and Deep Link Router scenes.
- **Samples**: Added one importable **Mobile Services Samples** bundle containing independently playable Playground, Haptics Palette, Notifications Scheduler, and Deep Link Router scenes.
- **Documentation**: Added subsystem references and editor-tooling guides for the Device Simulator and build pipeline.

**Changed**:
- Consolidated the package under the `com.gamelovers.mobileservices` package name, `GameLovers.MobileServices.*` namespaces, and `GameLovers.MobileServices` assembly.
- Updated the package baseline to Unity 6 and documented the supported 6000.5.7f1, 6000.3.21f1, and 6000.0.81f1 validation editors.

**Fixed**:
- Fixed persisted notifications so nullable IDs, badge numbers, and delivery times survive background/foreground rescheduling.
- Fixed local notification delivered and expired events so subscribers added after service construction receive callbacks.
- Fixed editor notification scheduling so generated notifications appear in the pending collection.
**Changed**:
- Consolidated the package under the `com.gamelovers.mobileservices` package name, `GameLovers.MobileServices.*` namespaces, and `GameLovers.MobileServices` assembly.
- Updated the package baseline to Unity 6 and documented the supported 6000.5.7f1, 6000.3.21f1, and 6000.0.81f1 validation editors.
**Fixed**:
- Fixed persisted notifications so nullable IDs, badge numbers, and delivery times survive background/foreground rescheduling.
- Fixed local notification delivered and expired events so subscribers added after service construction receive callbacks.
- Fixed editor notification scheduling so generated notifications appear in the pending collection.

**Removed**:
- Removed legacy tap detection; use the Unity Input System's `TapInteraction` instead.
Expand Down
72 changes: 64 additions & 8 deletions Editor/Explorer/Overlays/MobileSimulatorRuntimeOverlay.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Collections.Generic;
using GameLovers.MobileServices.NativeUi;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
Expand All @@ -11,14 +13,13 @@ namespace GameLovers.MobileServices.Editor.Explorer.Overlays
/// device's <c>Screen.*</c> values.
/// </summary>
/// <remarks>
/// <para>The overlay is alive whenever the Device Simulator plugin panel is open (edit OR play
/// mode) so a designer can fire a mock from the panel and see it inside the simulated phone
/// without entering play mode. A single idempotent <see cref="RefreshLifecycle"/> drives spawn /
/// teardown.</para>
/// <para>The overlay is alive whenever the Device Simulator plugin panel is open or a runtime
/// alert is visible, including a plain Game view with no simulator window. A single idempotent
/// <see cref="RefreshLifecycle"/> drives spawn / teardown.</para>
/// <para>The overlay renders inside Unity's runtime UIToolkit panel - <see cref="UIDocument"/>
/// is <c>[ExecuteAlways]</c>, so its panel paints into the Game / Device Simulator view in edit
/// mode too. Interaction inside the mock is unreliable in the edit-mode Game view, so dismissal
/// is driven from the plugin panel; the overlay is treated as display-only.</para>
/// mode too. Runtime play-mode alerts are interactive; edit-mode panel previews retain their
/// panel-owned dismissal controls.</para>
/// <para>The <see cref="PanelSettings"/> instance is constructed programmatically (rather than
/// shipped as a <c>.asset</c>) to keep the setup editor-only by construction.</para>
/// </remarks>
Expand All @@ -34,14 +35,17 @@ internal static class MobileSimulatorRuntimeOverlay
private static OverlayController _controller;
private static PanelSettings _panelSettings;
private static bool _pluginActive;
private static bool _standaloneAlertActive;

private static bool ShouldBeAlive => _pluginActive;
private static bool ShouldBeAlive => _pluginActive || _standaloneAlertActive;

static MobileSimulatorRuntimeOverlay()
{
// Re-evaluate across play-mode transitions so the host's DontDestroyOnLoad / teardown is
// applied correctly when the panel is open while entering or exiting play mode.
EditorApplication.playModeStateChanged += _ => RefreshLifecycle();
NativeUiService.EditorShowAlertOverride = ShowAlert;
NativeUiService.EditorDismissAlertOverride = DismissAlert;
}

/// <summary>
Expand All @@ -55,6 +59,51 @@ internal static void NotifyPluginActive(bool active)
RefreshLifecycle();
}

/// <summary>Paints one runtime-requested alert through the Editor simulator overlay.</summary>
internal static void ShowAlert(
bool isAlertSheet,
bool isDismissible,
string title,
string message,
AlertButton[] buttons)
{
_standaloneAlertActive = true;
EnsureSpawned();

var simulatedButtons = new List<SimulatedAlertButton>(buttons.Length);
foreach (var button in buttons)
{
simulatedButtons.Add(new SimulatedAlertButton
{
Text = button.Text,
Style = (SimulatedAlertButtonStyle)button.Style,
OnClicked = button.Callback,
});
}

MobileSimulatorState.PushAlert(new SimulatedAlertSpec
{
Title = title,
Message = message,
IsActionSheet = isAlertSheet,
IsDismissible = isDismissible,
Buttons = simulatedButtons,
});
}

/// <summary>Dismisses the active Editor alert without invoking an action.</summary>
internal static void DismissAlert()
{
if (_controller != null)
{
MobileSimulatorState.PushDismissAll();
return;
}

_standaloneAlertActive = false;
RefreshLifecycle();
}

private static void RefreshLifecycle()
{
if (ShouldBeAlive)
Expand Down Expand Up @@ -323,7 +372,7 @@ private void OnAlert(SimulatedAlertSpec spec)
{
ClearStage();
ShowStage();
_stage.Add(MockBuilders.BuildAlert(MobileSimulatorState.Platform, spec, ClearStage));
_stage.Add(MockBuilders.BuildAlert(MobileSimulatorState.Platform, spec, DismissAlert));
}

private void OnToast(SimulatedToastSpec spec)
Expand Down Expand Up @@ -384,8 +433,15 @@ private void OnPermissionDialog(SimulatedPermissionDialogSpec spec)
}

private void OnDismissAll()
{
DismissAlert();
}

private void DismissAlert()
{
ClearStage();
_standaloneAlertActive = false;
EditorApplication.delayCall += RefreshLifecycle;
}

private void ShowStage()
Expand Down
1 change: 1 addition & 0 deletions Editor/Explorer/Overlays/MobileSimulatorState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public sealed class SimulatedAlertSpec
public string Title;
public string Message;
public bool IsActionSheet;
public bool IsDismissible = true;
public List<SimulatedAlertButton> Buttons = new List<SimulatedAlertButton>();
}

Expand Down
2 changes: 1 addition & 1 deletion Editor/Explorer/Overlays/MockBuilders.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ internal static VisualElement BuildAlert(SimulatedPlatform platform, SimulatedAl
{
var btn = new Button(() =>
{
btnSpec.OnClicked?.Invoke();
dismissCallback?.Invoke();
btnSpec.OnClicked?.Invoke();
}) { text = btnSpec.Text };
btn.AddToClassList("mock-card-button");
switch (btnSpec.Style)
Expand Down
4 changes: 4 additions & 0 deletions Editor/Simulation/EditorPlatformSimulator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ public static void Engage()
PermissionsService.EditorRequestAsyncOverride = RequestPermissionAsync;
AttService.EditorCurrentStatusOverride = ReadAttStore();
AttService.EditorRequestAsyncOverride = RequestAttAsync;
NativeUiService.EditorShowAlertOverride = MobileSimulatorRuntimeOverlay.ShowAlert;
NativeUiService.EditorDismissAlertOverride = MobileSimulatorRuntimeOverlay.DismissAlert;
// Review is fire-and-forget (no OS success callback) — the same RequestReview() the game
// calls drives the overlay mock in edit + play mode, mirroring the Permissions / ATT hooks.
NativeUiService.EditorRequestReviewOverride = () => MobileSimulatorState.PushReview();
Expand All @@ -155,6 +157,8 @@ public static void Disengage()
AttService.EditorCurrentStatusOverride = null;
AttService.EditorRequestResultOverride = null;
AttService.EditorRequestAsyncOverride = null;
NativeUiService.EditorShowAlertOverride = MobileSimulatorRuntimeOverlay.ShowAlert;
NativeUiService.EditorDismissAlertOverride = MobileSimulatorRuntimeOverlay.DismissAlert;
NativeUiService.EditorRequestReviewOverride = null;
_pendingPermissionResolvers.Clear();
_pendingAttResolver = null;
Expand Down
25 changes: 23 additions & 2 deletions Plugins/iOS/NativeUi.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

typedef void (*AlertButtonCallback)(const char * str);

static UIAlertController *GameLoversCurrentAlert;

void _GameLoversAlertMessage (bool isSheet, char* title, char* message, char* buttonsText[], int buttonsStyle[], int buttonsLength, AlertButtonCallback buttonCallback)
{
UIAlertControllerStyle style = isSheet ? UIAlertControllerStyleActionSheet : UIAlertControllerStyleAlert;
Expand All @@ -17,16 +19,35 @@ void _GameLoversAlertMessage (bool isSheet, char* title, char* message, char* bu
for (int i = 0; i < buttonsLength; i++)
{
NSString *buttonText = ToNSString(buttonsText[i]);
int index = i;
UIAlertAction * button = [UIAlertAction actionWithTitle:buttonText style:(UIAlertActionStyle)buttonsStyle[i] handler:^(UIAlertAction * action)
{
GameLoversCurrentAlert = nil;
buttonCallback((char*)[buttonText UTF8String]);
}];
[alert addAction:button];
}

dispatch_async(dispatch_get_main_queue(), ^{
[UnityGetGLViewController() presentViewController:alert animated:YES completion:nil];
void (^presentAlert)(void) = ^{
GameLoversCurrentAlert = alert;
[UnityGetGLViewController() presentViewController:alert animated:YES completion:nil];
};
if (GameLoversCurrentAlert != nil)
{
[GameLoversCurrentAlert dismissViewControllerAnimated:NO completion:presentAlert];
}
else
{
presentAlert();
}
});
}

void _GameLoversDismissAlert(void)
{
dispatch_async(dispatch_get_main_queue(), ^{
[GameLoversCurrentAlert dismissViewControllerAnimated:YES completion:nil];
GameLoversCurrentAlert = nil;
});
}

Expand Down
Loading
Loading