Automated E2E Tests on all backend environments (#5712)

* Testing maestro e2e

* Testing e2e

* Testing e2e

* Testing e2e

* Testing e2e

* Testing e2e

* Testing e2e

* Implemented Fastlane action for installing the right StoreKit config file to the simulator that will be used for maestro testing

* Made the setup simulator storekit config action a lot more stable by updating the code finding the storekit shared app group container

* Added script for generating CI.xcconfig file to be used by the CI runs

* Updated config.yml

* Updated maestro flows path

* Made the e2e test lane reusable with a test_environment argument, added storekit config file for load shedder. Setup CircleCI job for all 3 test_environments

* Fix passing load shedder api key to command

* Updated storing of test output

* Added test environment name to the e2e test runs

* Restore missing install_dependencies call

* Also store test output as artifacts

* Allow setting force_server_error_strategy from env variable, using it in the fallback e2e tests.

* Using CircleCI matrix for test_environments

* Added support for showing different E2E flows in the app depending on which maestro flow is run to avoid tests breaking if changes are made

* Using new automated sdk project. Using a single test flow and the environment is now soley based on API key and server error strategy environment variables

* Updated context and environment variables for e2e tests

* Only generate maestro workspace

* Test

* Fixed incorrect env variables being used

* Fix formatting

* Simplified env var -> xcconfig logic to avoid using wrong values

* Fix config

* Updated maestro screenshot names

* Fixed forceServerErrorStrategy

* Reusing maestro e2e test all environments

* Added fortress 2 as load_shedder_secondary test environment

* Rename test_environment to backend_environment

* use default value for force_server_error_strategy

* Updated flow names

* Added daily e2e test schedule

* Removed unused parameter

* Revert to original build and test step after testing

* Passing the api_host to the maestro test app for the load shedder backends, using the normal API key

* Renamed load_shedder_primary and load_shedder_secondary

* Renamed forceServerErrorStrategy case and removed protocl from api_host

* Small code changes

* Rename parameter target to query to align with Tuist terminology

* Small code changes to address PR feedback
This commit is contained in:
Rick authored and GitHub committed 2025-11-10 11:25:29 +01:00
1 parent e0cb35205c
commit 20cea87fec
18 files changed
+579 -11

No files matched your search

+131 -2
View File
@@ -1,7 +1,7 @@
orbs:
macos: circleci/macos@2.5.1
slack: circleci/slack@5.0.0
revenuecat: revenuecat/sdks-common-config@3.2.0
revenuecat: revenuecat/sdks-common-config@3.9.0
# Disabled until compatible with M1: codecov: codecov/codecov@3.3.0
# codecov: codecov/codecov@3.3.0
@@ -116,6 +116,15 @@ aliases:
ignore: /.*/
branches:
only: main
maestro-e2e-test-job: &maestro-e2e-test-job
run-maestro-e2e-tests:
name: run-maestro-e2e-tests-<< matrix.backend_environment >>
context:
- e2e-tests
- slack-secrets-ios
matrix:
parameters:
backend_environment: ["production", "load_shedder_us_east_1", "load_shedder_us_east_2", "fallback"]
commands:
slack-notify-on-fail:
@@ -413,13 +422,22 @@ commands:
tuist-generate-workspace:
description: "Install Tuist and generate workspace"
parameters:
query:
type: string
default: ""
description: "Optional query to pass to Tuist when generating workspace."
steps:
- revenuecat/install-tuist
- run:
name: Generate Tuist workspace
command: |
export PATH="$HOME/.local/share/mise/shims:$PATH"
tuist install && tuist generate --no-open
if [ -n "<< parameters.query >>" ]; then
tuist install && tuist generate << parameters.query >> --no-open
else
tuist install && tuist generate --no-open
fi
create-snapshot-pr-if-needed:
parameters:
@@ -480,6 +498,50 @@ commands:
- revenuecat/set-ssh-key-for-git:
fingerprint: "SHA256:UYi3gsOInLRPZy6MPho7Yr/YK1VmjDvPGo7sJvIcnPM"
setup_xcconfig:
parameters:
api_key:
type: string
description: "The RevenueCat API key value"
force_server_error_strategy:
type: enum
enum: ["never", "primary_backend_down"]
default: "never"
description: "Server error strategy for testing fallback behavior"
api_host:
type: string
default: ""
description: "Optional API host for testing (e.g., 'api-staging.revenuecat.com')"
proxy_url_scheme:
type: string
default: ""
description: "Optional proxy URL scheme (e.g., 'https')"
proxy_url_host:
type: string
default: ""
description: "Optional proxy URL host (e.g., 'proxy.revenuecat.com')"
steps:
- run:
name: Create XCConfig file
command: |
{
echo "// Auto-generated. Do not commit to VCS"
echo "// Generated on $(date)"
echo ""
echo "REVENUECAT_API_KEY = << parameters.api_key >>"
echo "REVENUECAT_FORCE_SERVER_ERROR_STRATEGY = << parameters.force_server_error_strategy >>"
if [ -n "<< parameters.api_host >>" ]; then
echo "REVENUECAT_API_HOST = << parameters.api_host >>"
fi
if [ -n "<< parameters.proxy_url_scheme >>" ]; then
echo "REVENUECAT_PROXY_URL_SCHEME = << parameters.proxy_url_scheme >>"
fi
if [ -n "<< parameters.proxy_url_host >>" ]; then
echo "REVENUECAT_PROXY_URL_HOST = << parameters.proxy_url_host >>"
fi
} > CI.xcconfig
echo "✅ Created CI.xcconfig"
jobs:
validate-package-swift:
parameters:
@@ -1135,6 +1197,64 @@ jobs:
test_plan: "BackendIntegrationTests-CustomEntitlements"
- slack-notify-on-fail
run-maestro-e2e-tests:
parameters:
backend_environment:
type: enum
enum: ["production", "load_shedder_us_east_1", "load_shedder_us_east_2", "fallback"]
executor:
name: macos-executor
xcode_version: "26.0.1"
steps:
- macos/preboot-simulator:
version: "18.6"
platform: "iOS"
device: "iPhone 16 Pro"
- checkout
- install-dependencies:
install_swiftlint: false
- when:
condition:
equal: [ "load_shedder_us_east_1", << parameters.backend_environment >> ]
steps:
- setup_xcconfig:
api_key: ${RC_E2E_TEST_API_KEY_PRODUCTION_APP_STORE}
api_host: fortress-us-east-1.revenuecat.com
- when:
condition:
equal: [ "load_shedder_us_east_2", << parameters.backend_environment >> ]
steps:
- setup_xcconfig:
api_key: ${RC_E2E_TEST_API_KEY_PRODUCTION_APP_STORE}
api_host: fortress-us-east-2.revenuecat.com
- when:
condition:
equal: [ "fallback", << parameters.backend_environment >> ]
steps:
- setup_xcconfig:
api_key: ${RC_E2E_TEST_API_KEY_PRODUCTION_APP_STORE}
force_server_error_strategy: primary_backend_down
- when:
condition:
equal: [ "production", << parameters.backend_environment >> ]
steps:
- setup_xcconfig:
api_key: ${RC_E2E_TEST_API_KEY_PRODUCTION_APP_STORE}
- revenuecat/install-maestro
- tuist-generate-workspace:
query: "Maestro"
- run:
name: Run e2e tests (<< parameters.backend_environment >>)
command: bundle exec fastlane run_maestro_e2e_tests
no_output_timeout: 15m
# todo: implement error / success notifications through Slack
- store_test_results:
path: fastlane/test_output
- store_artifacts:
path: fastlane/test_output
release-checks:
executor:
name: macos-executor
@@ -1663,6 +1783,14 @@ workflows:
- slack-secrets
- integration-tests-all
daily-maestro-e2e-tests:
when:
and:
- equal: [scheduled_pipeline, << pipeline.trigger_source >>]
- equal: ["maestro_e2e_tests", << pipeline.schedule.name >>]
jobs:
- <<: *maestro-e2e-test-job
manual-trigger-bump:
when:
equal: [bump, << pipeline.parameters.action >>]
@@ -1712,6 +1840,7 @@ workflows:
- equal:
["run-from-github-comments", << pipeline.parameters.GHA_Meta >>]
jobs:
- <<: *maestro-e2e-test-job
- backend-integration-tests-SK1:
context:
- slack-secrets-ios
+3
View File
@@ -66,6 +66,9 @@ fastlane/.env
# Local build settings
Local.xcconfig
# CI Build settings
CI.xcconfig
# Gets created when Cursor is used with the sswg Swift extension
/.index-build/
+3 -1
View File
@@ -21,7 +21,9 @@ let project = Project(
],
"REVENUECAT_API_KEY": "$(REVENUECAT_API_KEY)",
"REVENUECAT_PROXY_URL_SCHEME": "$(REVENUECAT_PROXY_URL_SCHEME)",
"REVENUECAT_PROXY_URL_HOST": "$(REVENUECAT_PROXY_URL_HOST)"
"REVENUECAT_PROXY_URL_HOST": "$(REVENUECAT_PROXY_URL_HOST)",
"REVENUECAT_FORCE_SERVER_ERROR_STRATEGY": "$(REVENUECAT_FORCE_SERVER_ERROR_STRATEGY)",
"REVENUECAT_API_HOST": "$(REVENUECAT_API_HOST)"
]
),
sources: ["rc-maestro/Sources/**/*.swift"],
@@ -0,0 +1,21 @@
appId: com.revenuecat.maestro.ios
name: Subscribe to product through V1 Paywall for alternative offering
---
- launchApp:
arguments:
e2e_test_flow: subscribe_from_v1_paywall
- tapOn:
text: "Present Paywall"
- assertVisible: "Paywall V1"
- takeScreenshot: "subscribe_from_v1_paywall - Paywall"
- tapOn:
text: "Monthly"
- tapOn:
text: "Continue"
- takeScreenshot: "subscribe_from_v1_paywall - IAP Sheet"
- tapOn:
text: "Subscribe"
- takeScreenshot: "subscribe_from_v1_paywall- IAP Confirmation"
- tapOn:
text: "OK"
@@ -0,0 +1,21 @@
appId: com.revenuecat.maestro.ios
name: Subscribe to product through V2 Paywall for default offering
---
- launchApp:
arguments:
e2e_test_flow: subscribe_from_v2_paywall
- tapOn:
text: "Present Paywall"
- assertVisible: "Paywall V2"
- takeScreenshot: "subscribe_from_v2_paywall - Paywall"
- tapOn:
text: "Yearly"
- tapOn:
text: "Continue"
- takeScreenshot: "subscribe_from_v2_paywall - IAP Sheet"
- tapOn:
text: "Subscribe"
- takeScreenshot: "subscribe_from_v2_paywall - IAP Confirmation"
- tapOn:
text: "OK"
@@ -0,0 +1 @@
{"appPolicies":{"eula":"","policies":[{"locale":"en_US","policyText":"","policyURL":""}]},"identifier":"517FAB12","nonRenewingSubscriptions":[],"products":[],"subscriptionGroups":[{"id":"3723925D","localizations":[],"name":"Default offering","subscriptions":[{"adHocOffers":[],"codeOffers":[],"displayPrice":"9.99","familyShareable":false,"groupNumber":1,"internalID":"3F6716CC","localizations":[{"description":"","displayName":"","locale":"en_US"}],"productID":"pro_monthly_subscription","recurringSubscriptionPeriod":"P1M","referenceName":"Pro monthly","subscriptionGroupID":"3723925D","type":"RecurringSubscription","winbackOffers":[]},{"adHocOffers":[],"codeOffers":[],"displayPrice":"79.99","familyShareable":false,"groupNumber":1,"internalID":"42EDF3B1","localizations":[{"description":"","displayName":"","locale":"en_US"}],"productID":"pro_annual_subscription","recurringSubscriptionPeriod":"P1Y","referenceName":"Pro annually","subscriptionGroupID":"3723925D","type":"RecurringSubscription","winbackOffers":[]}]}],"version":{"major":4,"minor":0}}
@@ -34,4 +34,36 @@ enum Constants {
}
return "\(scheme)//\(host)"
}()
/*
The force server error strategy to configure Purchases with
To be used in (e2e) tests in order to simulate server failures
REVENUECAT_FORCE_SERVER_ERROR_STRATEGY = primary_domain_down
*/
static var forceServerErrorStrategy: Constants.ForceServerErrorStrategy {
guard let value = Bundle.main.object(forInfoDictionaryKey: "REVENUECAT_FORCE_SERVER_ERROR_STRATEGY") as? String else {
return .never
}
return ForceServerErrorStrategy(rawValue: value) ?? .never
}
/*
The API base URL to configure SystemInfo with
To be used in (e2e) tests in order to test against different environments
REVENUECAT_API_HOST = api-staging.revenuecat.com
*/
static var apiBaseURL: URL? {
guard let host = Bundle.main.object(forInfoDictionaryKey: "REVENUECAT_API_HOST") as? String,
!host.isEmpty,
let url = URL(string: "https://" + host) else {
return nil
}
return url
}
enum ForceServerErrorStrategy: String {
case primaryBackendDown = "primary_backend_down"
case never
}
}
@@ -27,7 +27,7 @@ public struct ContentView: View {
pushCustomerCenter = true
}
.buttonStyle(.borderedProminent)
Spacer()
}
.navigationDestination(isPresented: $pushCustomerCenter, destination: {
@@ -1,5 +1,5 @@
import SwiftUI
import RevenueCat
@testable import RevenueCat
@main
struct RcMaestroApp: App {
@@ -7,12 +7,68 @@ struct RcMaestroApp: App {
init() {
Purchases.logLevel = .verbose
Purchases.proxyURL = Constants.proxyURL.flatMap { URL(string: $0) }
Purchases.configure(withAPIKey: Constants.apiKey)
// Set API base URL if provided (used in E2E tests)
if let apiBaseURL = Constants.apiBaseURL {
SystemInfo.apiBaseURL = apiBaseURL
}
// Used in E2E tests
Purchases.configure(
with: .builder(withAPIKey: Constants.apiKey)
.with(dangerousSettings: .init(
autoSyncPurchases: true,
internalSettings: DangerousSettings.Internal(
forceServerErrorStrategy: .init { request in
switch Constants.forceServerErrorStrategy {
case .never:
return false
case .primaryBackendDown:
return request.fallbackUrlIndex == nil
}
}
)
))
.build()
)
}
var body: some Scene {
WindowGroup {
ContentView()
switch e2eTestFlow {
case .some(let flow):
flow.view
case nil:
ContentView()
}
}
}
/*
Parses the launch argument with the e2e test flow to run
*/
fileprivate var e2eTestFlow: E2ETestFlow? {
guard let string = UserDefaults.standard.dictionaryRepresentation()["e2e_test_flow"] as? String else {
return nil
}
return E2ETestFlow(rawValue: string)
}
}
enum E2ETestFlow: String {
case subscribeFromV1Paywall = "subscribe_from_v1_paywall"
case subscribeFromV2Paywall = "subscribe_from_v2_paywall"
@ViewBuilder
var view: some View {
switch self {
case .subscribeFromV1Paywall:
E2ETestFlowView.SubscribeFromV1Paywall()
case .subscribeFromV2Paywall:
E2ETestFlowView.SubscribeFromV2Paywall()
}
}
}
enum E2ETestFlowView {}
@@ -0,0 +1,46 @@
//
// subscribe-from-v1-paywall.swift
// Maestro
//
// Created by Rick van der Linden on 30/10/2025.
// Copyright © 2025 RevenueCat, Inc. All rights reserved.
//
import SwiftUI
import RevenueCat
import RevenueCatUI
extension E2ETestFlowView {
struct SubscribeFromV1Paywall: View {
@State private var offering: Offering?
@State private var presentPaywall = false
var body: some View {
VStack {
Text("V1 Paywall - alternative offering")
.font(.largeTitle)
if offering != nil {
Button("Present Paywall") {
presentPaywall = true
}
.buttonStyle(.borderedProminent)
}
else {
Text("Loading offerings...")
}
}
.task {
let offerings = try? await Purchases.shared.offerings()
self.offering = offerings?.offering(identifier: "paywall_v1")
}
.sheet(isPresented: $presentPaywall) {
if let offering {
PaywallView(offering: offering)
}
}
.multilineTextAlignment(.center)
}
}
}
@@ -0,0 +1,33 @@
//
// subscribe-from-v2-paywall.swift
// Maestro
//
// Created by Rick van der Linden on 30/10/2025.
// Copyright © 2025 RevenueCat, Inc. All rights reserved.
//
import SwiftUI
import RevenueCat
import RevenueCatUI
extension E2ETestFlowView {
struct SubscribeFromV2Paywall: View {
@State private var presentPaywall = false
var body: some View {
VStack {
Text("V2 Paywall - default offering")
.font(.largeTitle)
Button("Present Paywall") {
presentPaywall = true
}
.buttonStyle(.borderedProminent)
}
.sheet(isPresented: $presentPaywall) {
PaywallView()
}
.multilineTextAlignment(.center)
}
}
}
+1
View File
@@ -24,4 +24,5 @@
// Note that this file is read by the RevenueCat, PurchaseTester and PaywallsTester projects.
// ======
#include? "CI.xcconfig"
#include? "Local.xcconfig"
+5 -1
View File
@@ -37,4 +37,8 @@
// REVENUECAT_PROXY_URL_SCHEME = http
// REVENUECAT_PROXY_URL_HOST = localhost:9000
// Because two slashes are interpreted as a comment in this file, the scheme and host need to be set separately
// Because two slashes are interpreted as a comment in this file, the scheme and host need to be set separately
// The following configuration options can be used when running the E2E tests through the Tuist generated Maestro project
// REVENUECAT_FORCE_SERVER_ERROR_STRATEGY = primary_backend_down / never
// REVENUECAT_API_HOST = custom-api-host.revenuecat.com
+11 -2
View File
@@ -9,13 +9,22 @@ import PackageDescription
/// flags defined in it. It does nothing if this file does not exist in this exact folder. This file does not exist on
/// a clean checkout. It has to be created manually by a developer.
var additionalCompilerFlags: [PackageDescription.SwiftSetting] = {
guard let config = try? String(
let ciConfig = try? String(
contentsOf: URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.appendingPathComponent("CI.xcconfig")
)
let localConfig = try? String(
contentsOf: URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.appendingPathComponent("Local.xcconfig")
) else {
)
guard let config = ciConfig ?? localConfig else {
return []
}
// We split the capture group by space and remove any special flags, such as $(inherited).
return config
.firstMatch(of: #/^SWIFT_ACTIVE_COMPILATION_CONDITIONS *= *(.*)$/#.anchorsMatchLineEndings())?
+3 -1
View File
@@ -27,7 +27,9 @@ var additionalFiles: [FileElement] = [
.glob(pattern: "Tests/TestPlans/**/*.xctestplan"),
.glob(pattern: "Tests/RevenueCatUITests/TestPlans/**/*.xctestplan")
]
if FileManager.default.fileExists(atPath: "Local.xcconfig") {
if FileManager.default.fileExists(atPath: "CI.xcconfig") {
additionalFiles.append(.glob(pattern: "CI.xcconfig"))
} else if FileManager.default.fileExists(atPath: "Local.xcconfig") {
additionalFiles.append(.glob(pattern: "Local.xcconfig"))
}
+50
View File
@@ -802,6 +802,56 @@ platform :ios do
configuration: 'release',
)
end
desc "run_maestro_e2e_tests"
lane :run_maestro_e2e_tests do |options|
bundle_id = "com.revenuecat.maestro.ios"
base_path = "#{Dir.pwd}/.."
UI.message("Running Maestro E2E tests")
storekit_config_path = "#{base_path}/Examples/rc-maestro/rc-maestro/Resources/StoreKit/app787ddb07e6.storekit"
# Verify StoreKit config exists
unless File.exist?(storekit_config_path)
UI.user_error!("StoreKit configuration not found: #{storekit_config_path}")
end
# note: it's called "run" tests but these tests only need to build to run
xcworkspace = 'RevenueCat-Tuist.xcworkspace'
unless File.directory?(xcworkspace)
UI.important("#{xcworkspace} not found. Run `tuist generate --no-open` before invoking this lane.")
end
xcodebuild(
workspace: xcworkspace,
scheme: "Maestro",
sdk: "iphonesimulator",
configuration: "Debug",
derivedDataPath: "./build"
)
sh("xcrun", "simctl", "install", "booted", "#{base_path}/build/Build/Products/Debug-iphonesimulator/Maestro.app")
# Setup StoreKit configuration in simulator
setup_simulator_storekit_config(
bundle_id: bundle_id,
storekit_config_path: storekit_config_path
)
# Construct the flow directory path
flow_dir = "#{base_path}/Examples/rc-maestro/maestro/e2e_tests"
# Verify the flow directory exists
unless File.directory?(flow_dir)
UI.user_error!("Flow directory not found: #{flow_dir}")
end
UI.message("Using flow directory: #{flow_dir}")
sh("maestro", "test", "--format", "junit", "--output", "test_output/maestro/report.xml", "--test-output-dir", "test_output/maestro", flow_dir)
end
desc "replace API KEY for installation and integration tests"
lane :replace_api_key_integration_tests do
+8
View File
@@ -218,6 +218,14 @@ archive all platforms
run API Tests
### ios run_maestro_e2e_tests
```sh
[bundle exec] fastlane ios run_maestro_e2e_tests
```
run_maestro_e2e_tests
### ios replace_api_key_integration_tests
```sh
@@ -0,0 +1,150 @@
module Fastlane
module Actions
class SetupSimulatorStorekitConfigAction < Action
def self.run(params)
bundle_id = params[:bundle_id]
storekit_config_path = params[:storekit_config_path]
# Verify StoreKit config exists
unless File.exist?(storekit_config_path)
UI.user_error!("StoreKit configuration not found at: #{storekit_config_path}")
end
UI.message("Setting up StoreKit configuration for app: #{bundle_id}")
# Get the app container path
container_path = Actions.sh("xcrun", "simctl", "get_app_container", "booted", bundle_id, log: false).strip
UI.message("App container path: #{container_path}")
# Extract the base path up to and including "data/Containers"
unless container_path =~ /(.*\/data\/Containers)\//
UI.user_error!("Could not extract base path from: #{container_path}")
end
base_container_path = $1
UI.message("Base container path: #{base_container_path}")
# Find the correct app group container by checking metadata plist
app_group_pattern = "#{base_container_path}/Shared/AppGroup/*"
UI.message("Searching for app group containers: #{app_group_pattern}")
app_group_dirs = Dir.glob(app_group_pattern)
if app_group_dirs.empty?
UI.user_error!("No app group containers found at: #{app_group_pattern}")
end
# Find the app group with MCMMetadataIdentifier = group.com.apple.storekit
correct_app_group = app_group_dirs.find do |app_group_dir|
metadata_plist = File.join(app_group_dir, ".com.apple.mobile_container_manager.metadata.plist")
next false unless File.exist?(metadata_plist)
begin
# Use plutil to extract the MCMMetadataIdentifier value directly
identifier = Actions.sh("plutil", "-extract", "MCMMetadataIdentifier", "raw", "-o", "-", metadata_plist, log: false).strip
identifier == "group.com.apple.storekit"
rescue => e
UI.verbose("Failed to read plist at #{metadata_plist}: #{e.message}")
false
end
end
if correct_app_group.nil?
UI.user_error!("Could not find StoreKit app group container (MCMMetadataIdentifier == group.com.apple.storekit)")
end
UI.success("Found StoreKit app group container: #{correct_app_group}")
# Create the Octane folder if it doesn't exist
octane_path = File.join(correct_app_group, "Documents", "Persistence", "Octane")
unless File.directory?(octane_path)
UI.message("Octane folder doesn't exist, creating it...")
Actions.sh("mkdir", "-p", octane_path, log: false)
end
UI.success("Octane path: #{octane_path}")
# Construct the destination paths
storekit_dir = File.join(octane_path, bundle_id, "Configuration.storekit")
storekit_file = File.join(storekit_dir, "Configuration.storekit")
UI.message("Creating directory: #{storekit_dir}")
Actions.sh("mkdir", "-p", storekit_dir, log: false)
UI.message("Copying StoreKit configuration...")
Actions.sh("cp", storekit_config_path, storekit_file, log: false)
# Verify
unless File.exist?(storekit_file)
UI.user_error!("Failed to copy StoreKit configuration")
end
UI.success("✅ StoreKit configuration installed at:")
UI.message(" #{storekit_file}")
storekit_file
end
def self.description
"Installs a StoreKit configuration file in the correct location for a simulator app to use during testing"
end
def self.details
"This action copies a StoreKit configuration file to the Octane directory structure " \
"within the iOS simulator, allowing the app to use local StoreKit testing without " \
"connecting to the App Store. The action finds the correct app group container by " \
"reading .com.apple.mobile_container_manager.metadata.plist and checking for " \
"MCMMetadataIdentifier == 'group.com.apple.storekit', creates the Documents/Persistence/Octane " \
"folder if needed, and places the configuration file at: " \
"data/Containers/Shared/AppGroup/{UUID}/Documents/Persistence/Octane/{bundle_id}/Configuration.storekit/Configuration.storekit"
end
def self.available_options
[
FastlaneCore::ConfigItem.new(
key: :bundle_id,
description: "The bundle identifier of the app installed in the simulator",
type: String,
optional: false
),
FastlaneCore::ConfigItem.new(
key: :storekit_config_path,
description: "Path to the StoreKit configuration file (.storekit)",
type: String,
optional: false,
verify_block: proc do |value|
UI.user_error!("StoreKit config file not found at '#{value}'") unless File.exist?(value)
UI.user_error!("File must be a .storekit file") unless value.end_with?('.storekit')
end
)
]
end
def self.return_value
"The full path to the installed StoreKit configuration file"
end
def self.authors
["rickvdl"]
end
def self.is_supported?(platform)
[:ios].include?(platform)
end
def self.example_code
[
'setup_simulator_storekit_config(
bundle_id: "com.example.myapp",
storekit_config_path: "path/to/StoreKitConfiguration.storekit"
)'
]
end
def self.category
:testing
end
end
end
end