diff --git a/e2e-tests/MaestroTestApp/Assets/Editor/BuildScript.cs b/e2e-tests/MaestroTestApp/Assets/Editor/BuildScript.cs new file mode 100644 index 00000000..24cf626b --- /dev/null +++ b/e2e-tests/MaestroTestApp/Assets/Editor/BuildScript.cs @@ -0,0 +1,105 @@ +using UnityEditor; +using UnityEditor.Build.Reporting; +using System; +using System.IO; +using System.Linq; + +static class BuildScript +{ + private static readonly string BuildPathIOS = "build/ios"; + private static readonly string BuildPathAndroid = "build/android/MaestroTestApp.apk"; + + static string[] GetEnabledScenes() + { + return ( + from scene in EditorBuildSettings.scenes + where scene.enabled + where !string.IsNullOrEmpty(scene.path) + select scene.path + ).ToArray(); + } + + [MenuItem("Build/Build iOS")] + public static void BuildIOS() + { + var scenes = GetEnabledScenes(); + if (scenes.Length == 0) + { + Console.WriteLine(":: No scenes found in EditorBuildSettings, looking for scene files..."); + scenes = Directory.GetFiles("Assets/Scenes", "*.unity", SearchOption.AllDirectories); + } + + if (scenes.Length == 0) + { + throw new Exception("No scenes found to build."); + } + + Console.WriteLine(":: Building iOS with scenes:"); + foreach (var scene in scenes) + { + Console.WriteLine(":: " + scene); + } + + var buildPlayerOptions = new BuildPlayerOptions + { + scenes = scenes, + locationPathName = BuildPathIOS, + target = BuildTarget.iOS, + options = BuildOptions.None + }; + + var report = BuildPipeline.BuildPlayer(buildPlayerOptions); + + if (report.summary.result != BuildResult.Succeeded) + { + throw new Exception("Build failed with " + report.summary.totalErrors + " error(s)"); + } + + Console.WriteLine(":: Build succeeded. Output: " + BuildPathIOS); + } + + [MenuItem("Build/Build Android")] + public static void BuildAndroid() + { + var scenes = GetEnabledScenes(); + if (scenes.Length == 0) + { + Console.WriteLine(":: No scenes found in EditorBuildSettings, looking for scene files..."); + scenes = Directory.GetFiles("Assets/Scenes", "*.unity", SearchOption.AllDirectories); + } + + if (scenes.Length == 0) + { + throw new Exception("No scenes found to build."); + } + + Console.WriteLine(":: Building Android with scenes:"); + foreach (var scene in scenes) + { + Console.WriteLine(":: " + scene); + } + + var buildDir = Path.GetDirectoryName(BuildPathAndroid); + if (!Directory.Exists(buildDir)) + { + Directory.CreateDirectory(buildDir); + } + + var buildPlayerOptions = new BuildPlayerOptions + { + scenes = scenes, + locationPathName = BuildPathAndroid, + target = BuildTarget.Android, + options = BuildOptions.None + }; + + var report = BuildPipeline.BuildPlayer(buildPlayerOptions); + + if (report.summary.result != BuildResult.Succeeded) + { + throw new Exception("Build failed with " + report.summary.totalErrors + " error(s)"); + } + + Console.WriteLine(":: Build succeeded. Output: " + BuildPathAndroid); + } +} diff --git a/e2e-tests/MaestroTestApp/Assets/Scripts/MaestroTestApp.cs b/e2e-tests/MaestroTestApp/Assets/Scripts/MaestroTestApp.cs new file mode 100644 index 00000000..2d53f426 --- /dev/null +++ b/e2e-tests/MaestroTestApp/Assets/Scripts/MaestroTestApp.cs @@ -0,0 +1,103 @@ +using UnityEngine; +using UnityEngine.UI; +using RevenueCatUI; + +public class MaestroTestApp : Purchases.UpdatedCustomerInfoListener +{ + private const string API_KEY = "MAESTRO_TESTS_REVENUECAT_API_KEY"; + + public GameObject testCasesScreen; + public GameObject purchaseScreen; + public Text entitlementsLabel; + public Text errorLabel; + + private Purchases purchases; + + void Start() + { + purchases = GetComponent(); + purchases.useRuntimeSetup = true; + + var config = Purchases.PurchasesConfiguration.Builder.Init(API_KEY).Build(); + purchases.Configure(config); + purchases.SetLogLevel(Purchases.LogLevel.Verbose); + purchases.listener = this; + + if (errorLabel != null) + { + errorLabel.gameObject.SetActive(false); + } + + ShowTestCases(); + } + + public void ShowTestCases() + { + testCasesScreen.SetActive(true); + purchaseScreen.SetActive(false); + } + + public void ShowPurchaseScreen() + { + testCasesScreen.SetActive(false); + purchaseScreen.SetActive(true); + ClearError(); + UpdateEntitlements(); + } + + public async void PresentPaywall() + { + ClearError(); + try + { + await PaywallsPresenter.Present(); + } + catch (System.Exception e) + { + Debug.LogError($"Failed to present paywall: {e}"); + ShowError(e.Message); + } + } + + public override void CustomerInfoReceived(CustomerInfo customerInfo) + { + UpdateEntitlementsFromInfo(customerInfo); + } + + private void UpdateEntitlements() + { + purchases.GetCustomerInfo((info, error) => + { + if (info != null) + { + UpdateEntitlementsFromInfo(info); + } + }); + } + + private void UpdateEntitlementsFromInfo(CustomerInfo info) + { + bool hasPro = info.Entitlements.Active.ContainsKey("pro"); + if (entitlementsLabel != null) + { + entitlementsLabel.text = "Entitlements: " + (hasPro ? "pro" : "none"); + } + } + + private void ShowError(string message) + { + if (errorLabel != null) + { + errorLabel.text = "Error: " + message; + errorLabel.gameObject.SetActive(true); + } + } + + private void ClearError() + { + if (errorLabel != null) + { + errorLabel.gameObject.SetActive(false); + } + } +} diff --git a/e2e-tests/MaestroTestApp/README.md b/e2e-tests/MaestroTestApp/README.md new file mode 100644 index 00000000..361ca7f4 --- /dev/null +++ b/e2e-tests/MaestroTestApp/README.md @@ -0,0 +1,45 @@ +# Maestro E2E Test App + +A minimal Unity app used by Maestro end-to-end tests to verify RevenueCat SDK integration. + +## Prerequisites + +- Unity Editor (with iOS Build Support and/or Android Build Support modules) +- Xcode (iOS) / Android Studio (Android) +- [Maestro](https://maestro.mobile.dev/) CLI + +## Setup + +This project requires manual scene setup in the Unity Editor because `.unity` scene +files cannot be created outside the editor. See [setup-instructions.md](./setup-instructions.md) +for detailed step-by-step instructions. + +## Running Locally + +1. Open the project in Unity Editor +2. File > Build Settings > select iOS or Android +3. Build and Run + +## API Key + +The app initialises RevenueCat with the placeholder `MAESTRO_TESTS_REVENUECAT_API_KEY`. +In CI, the Fastlane lane replaces this placeholder with the real key from the +`RC_E2E_TEST_API_KEY_PRODUCTION_TEST_STORE` environment variable (provided by the +CircleCI `e2e-tests` context) before building. + +To run locally, either: +- Replace the placeholder in `Assets/Scripts/MaestroTestApp.cs` with a valid API key + (do **not** commit it), or +- Export the env var and run the same `sed` command the Fastlane lane uses. + +## RevenueCat Project + +The test uses a RevenueCat project configured with: +- A **V2 Paywall** (the test asserts "Paywall V2" is visible) +- A `pro` entitlement (the test checks entitlement status after purchase) +- The **Test Store** environment for purchase confirmation + +## Dependencies + +The RevenueCat and RevenueCatUI Unity packages must be imported into the project +manually via the Unity Package Manager. diff --git a/e2e-tests/MaestroTestApp/setup-instructions.md b/e2e-tests/MaestroTestApp/setup-instructions.md new file mode 100644 index 00000000..b1fa5b3e --- /dev/null +++ b/e2e-tests/MaestroTestApp/setup-instructions.md @@ -0,0 +1,72 @@ +# MaestroTestApp Unity Scene Setup + +This app requires manual setup in the Unity Editor because `.unity` scene files +cannot be created outside the editor. + +## Prerequisites + +- Import the `RevenueCat` and `RevenueCatUI` packages into the project. + +## Scene Setup (MainScene) + +### 1. Create a Canvas + +- GameObject > UI > Canvas +- Set Canvas Scaler to "Scale With Screen Size", Reference Resolution 375x812. + +### 2. Add the Purchases component + +- Create an empty GameObject named "Purchases". +- Add the `Purchases` MonoBehaviour component to it. +- Add the `MaestroTestApp` script to the same GameObject. +- In the Inspector, check "Use Runtime Setup" on the Purchases component. + +### 3. Create testCasesScreen (child of Canvas) + +- Create an empty GameObject named "TestCasesScreen" under the Canvas. +- Add a RectTransform that fills the entire Canvas. +- Add a child **Text** (UI > Legacy > Text): + - Text content: `Test Cases` + - Font size: 24, bold, centered horizontally, near the top. +- Add a child **Button** (UI > Legacy > Button): + - Set the button's child Text to: `Purchase through paywall` + - On Click: drag the Purchases GameObject, select `MaestroTestApp > ShowPurchaseScreen`. + +### 4. Create purchaseScreen (child of Canvas) + +- Create an empty GameObject named "PurchaseScreen" under the Canvas. +- Add a RectTransform that fills the entire Canvas. +- Set it to **inactive** by default (uncheck the checkbox at the top of Inspector). +- Add a child **Text** named "EntitlementsLabel" (UI > Legacy > Text): + - Text content: `Entitlements: none` + - Font size: 16, centered. +- Add a child **Button** (UI > Legacy > Button): + - Set the button's child Text to: `Present Paywall` + - On Click: drag the Purchases GameObject, select `MaestroTestApp > PresentPaywall`. +- Add a child **Text** named "ErrorLabel" (UI > Legacy > Text): + - Text content: (leave empty) + - Font size: 14, color red, centered. + - Set it to **inactive** by default. +- Add a child **Button** for back navigation: + - Set the button's child Text to: `Back` + - On Click: drag the Purchases GameObject, select `MaestroTestApp > ShowTestCases`. + +### 5. Wire references in MaestroTestApp Inspector + +- Drag "TestCasesScreen" to the `testCasesScreen` field. +- Drag "PurchaseScreen" to the `purchaseScreen` field. +- Drag "EntitlementsLabel" Text to the `entitlementsLabel` field. +- Drag "ErrorLabel" Text to the `errorLabel` field. + +### 6. Project Settings + +- Edit > Project Settings > Player: + - Set Bundle Identifier to: `com.revenuecat.automatedsdktests` + - Set Product Name to: `MaestroTestApp` + +## CRITICAL: UI text must match exactly + +- Title: `Test Cases` +- Test case button: `Purchase through paywall` +- Entitlements label: `Entitlements: none` (before purchase) / `Entitlements: pro` (after) +- Paywall button: `Present Paywall`