Compare commits

..

No commits in common. "develop" and "1.2.1" have entirely different histories.

30 changed files with 156 additions and 563 deletions

View File

@ -1,41 +0,0 @@
# SelfieCam App Store Connect Copy
## Promotional Text (max 170)
Take better selfies in any light with a customizable screen ring light, pro camera controls, and quick timer capture.
Character count: 117 / 170
## Keywords (max 100)
selfie,camera,ring light,portrait,beauty,low light,timer,flash,creator,mirror,grid,hdr,photo editor
Character count: 99 / 100
## Description (max 4000)
SelfieCam is a professional selfie camera built for better lighting and cleaner results.
Perfect for creators, makeup artists, video calls, and anyone who wants more flattering selfies in any environment.
Core features:
- Customizable screen ring light with adjustable size, color, and brightness
- Front/back camera switching with smooth full-screen preview
- Flash controls (Off, On, Auto) plus front flash support
- Self-timer options with visual countdown
- Pinch-to-zoom and optional composition grid
- Post-capture preview with quick sharing
Pro features:
- Premium lighting presets and custom ring light colors
- HDR mode and higher photo quality options
- True Mirror mode
- Skin smoothing and advanced capture controls
- Extended timer options
Built with accessibility in mind:
- VoiceOver-friendly controls and labels
- Dynamic Type support for readable text
SelfieCam also supports iCloud settings sync, so your preferences stay consistent across devices.
Download SelfieCam and get studio-style selfie lighting straight from your screen.
Character count: 1055 / 4000

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 KiB

View File

@ -1,17 +0,0 @@
{
"folders": [
{
"path": "."
},
{
"path": "../_Packages/Bedrock"
}
],
"settings": {
"terminal.integrated.enablePersistentSessions": true,
"terminal.integrated.persistentSessionReviveProcess": "onExitAndWindowClose",
"task.allowAutomaticTasks": "off",
"swift.disableAutoResolve": true,
"swift.disableSwiftPackageManagerIntegration": true
}
}

View File

@ -7,9 +7,9 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
EA756A7A2F464B02006196BB /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EA756A792F464B02006196BB /* Bedrock */; };
EA836AF02F0AD00000077F87 /* RevenueCat in Frameworks */ = {isa = PBXBuildFile; productRef = EA836AEF2F0AD00000077F87 /* RevenueCat */; }; EA836AF02F0AD00000077F87 /* RevenueCat in Frameworks */ = {isa = PBXBuildFile; productRef = EA836AEF2F0AD00000077F87 /* RevenueCat */; };
EA836AF22F0AD00000077F87 /* RevenueCatUI in Frameworks */ = {isa = PBXBuildFile; productRef = EA836AF12F0AD00000077F87 /* RevenueCatUI */; }; EA836AF22F0AD00000077F87 /* RevenueCatUI in Frameworks */ = {isa = PBXBuildFile; productRef = EA836AF12F0AD00000077F87 /* RevenueCatUI */; };
EA836AF42F0AD00000077F87 /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EA836AF32F0AD00000077F87 /* Bedrock */; };
EA836AF62F0AD00000077F87 /* MijickCamera in Frameworks */ = {isa = PBXBuildFile; productRef = EA836AF52F0AD00000077F87 /* MijickCamera */; }; EA836AF62F0AD00000077F87 /* MijickCamera in Frameworks */ = {isa = PBXBuildFile; productRef = EA836AF52F0AD00000077F87 /* MijickCamera */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@ -32,32 +32,15 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
EA836ABF2F0ACE8A00077F87 /* Selfie Cam.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Selfie Cam.app"; sourceTree = BUILT_PRODUCTS_DIR; }; EA836ABF2F0ACE8A00077F87 /* Selfie Cam.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Selfie Cam.app"; sourceTree = BUILT_PRODUCTS_DIR; };
EA836ACC2F0ACE8B00077F87 /* SelfieCamTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SelfieCamTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; EA836ACC2F0ACE8B00077F87 /* Selfie Cam.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Selfie Cam.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
EA836AD62F0ACE8B00077F87 /* SelfieCamUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SelfieCamUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; EA836AD62F0ACE8B00077F87 /* Selfie Cam.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Selfie Cam.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
EACONFIG002 /* SelfieCam/Configuration/Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SelfieCam/Configuration/Debug.xcconfig; sourceTree = SOURCE_ROOT; }; EACONFIG002 /* SelfieCam/Configuration/Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SelfieCam/Configuration/Debug.xcconfig; sourceTree = SOURCE_ROOT; };
EACONFIG003 /* SelfieCam/Configuration/Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SelfieCam/Configuration/Release.xcconfig; sourceTree = SOURCE_ROOT; }; EACONFIG003 /* SelfieCam/Configuration/Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SelfieCam/Configuration/Release.xcconfig; sourceTree = SOURCE_ROOT; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
EAA5970C2F411B5D00BA5755 /* Exceptions for "SelfieCam" folder in "SelfieCam" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Configuration/Base.xcconfig,
Configuration/Debug.xcconfig,
Configuration/Release.xcconfig,
Configuration/Secrets.debug.xcconfig,
Configuration/Secrets.release.xcconfig,
);
target = EA836ABE2F0ACE8A00077F87 /* SelfieCam */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFileSystemSynchronizedRootGroup section */
EA836AC12F0ACE8A00077F87 /* SelfieCam */ = { EA836AC12F0ACE8A00077F87 /* SelfieCam */ = {
isa = PBXFileSystemSynchronizedRootGroup; isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
EAA5970C2F411B5D00BA5755 /* Exceptions for "SelfieCam" folder in "SelfieCam" target */,
);
path = SelfieCam; path = SelfieCam;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@ -79,7 +62,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
EA836AF62F0AD00000077F87 /* MijickCamera in Frameworks */, EA836AF62F0AD00000077F87 /* MijickCamera in Frameworks */,
EA756A7A2F464B02006196BB /* Bedrock in Frameworks */, EA836AF42F0AD00000077F87 /* Bedrock in Frameworks */,
EA836AF02F0AD00000077F87 /* RevenueCat in Frameworks */, EA836AF02F0AD00000077F87 /* RevenueCat in Frameworks */,
EA836AF22F0AD00000077F87 /* RevenueCatUI in Frameworks */, EA836AF22F0AD00000077F87 /* RevenueCatUI in Frameworks */,
); );
@ -117,8 +100,8 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
EA836ABF2F0ACE8A00077F87 /* Selfie Cam.app */, EA836ABF2F0ACE8A00077F87 /* Selfie Cam.app */,
EA836ACC2F0ACE8B00077F87 /* SelfieCamTests.xctest */, EA836ACC2F0ACE8B00077F87 /* Selfie Cam.xctest */,
EA836AD62F0ACE8B00077F87 /* SelfieCamUITests.xctest */, EA836AD62F0ACE8B00077F87 /* Selfie Cam.xctest */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@ -154,8 +137,8 @@
packageProductDependencies = ( packageProductDependencies = (
EA836AEF2F0AD00000077F87 /* RevenueCat */, EA836AEF2F0AD00000077F87 /* RevenueCat */,
EA836AF12F0AD00000077F87 /* RevenueCatUI */, EA836AF12F0AD00000077F87 /* RevenueCatUI */,
EA836AF32F0AD00000077F87 /* Bedrock */,
EA836AF52F0AD00000077F87 /* MijickCamera */, EA836AF52F0AD00000077F87 /* MijickCamera */,
EA756A792F464B02006196BB /* Bedrock */,
); );
productName = SelfieCam; productName = SelfieCam;
productReference = EA836ABF2F0ACE8A00077F87 /* Selfie Cam.app */; productReference = EA836ABF2F0ACE8A00077F87 /* Selfie Cam.app */;
@ -181,7 +164,7 @@
packageProductDependencies = ( packageProductDependencies = (
); );
productName = SelfieCamTests; productName = SelfieCamTests;
productReference = EA836ACC2F0ACE8B00077F87 /* SelfieCamTests.xctest */; productReference = EA836ACC2F0ACE8B00077F87 /* Selfie Cam.xctest */;
productType = "com.apple.product-type.bundle.unit-test"; productType = "com.apple.product-type.bundle.unit-test";
}; };
EA836AD52F0ACE8B00077F87 /* SelfieCamUITests */ = { EA836AD52F0ACE8B00077F87 /* SelfieCamUITests */ = {
@ -204,7 +187,7 @@
packageProductDependencies = ( packageProductDependencies = (
); );
productName = SelfieCamUITests; productName = SelfieCamUITests;
productReference = EA836AD62F0ACE8B00077F87 /* SelfieCamUITests.xctest */; productReference = EA836AD62F0ACE8B00077F87 /* Selfie Cam.xctest */;
productType = "com.apple.product-type.bundle.ui-testing"; productType = "com.apple.product-type.bundle.ui-testing";
}; };
/* End PBXNativeTarget section */ /* End PBXNativeTarget section */
@ -215,7 +198,7 @@
attributes = { attributes = {
BuildIndependentTargetsInParallel = 1; BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 2600; LastSwiftUpdateCheck = 2600;
LastUpgradeCheck = 2630; LastUpgradeCheck = 2600;
TargetAttributes = { TargetAttributes = {
EA836ABE2F0ACE8A00077F87 = { EA836ABE2F0ACE8A00077F87 = {
CreatedOnToolsVersion = 26.0; CreatedOnToolsVersion = 26.0;
@ -242,8 +225,8 @@
minimizedProjectReferenceProxies = 1; minimizedProjectReferenceProxies = 1;
packageReferences = ( packageReferences = (
EA836AEE2F0AD00000077F87 /* XCRemoteSwiftPackageReference "purchases-ios-spm" */, EA836AEE2F0AD00000077F87 /* XCRemoteSwiftPackageReference "purchases-ios-spm" */,
EA836AF72F0AD00000077F87 /* XCLocalSwiftPackageReference "../Bedrock" */,
EA836AF82F0AD00000077F87 /* XCRemoteSwiftPackageReference "MijickCamera" */, EA836AF82F0AD00000077F87 /* XCRemoteSwiftPackageReference "MijickCamera" */,
EA756A782F464B02006196BB /* XCLocalSwiftPackageReference "../_Packages/Bedrock" */,
); );
preferredProjectObjectVersion = 77; preferredProjectObjectVersion = 77;
productRefGroup = EA836AC02F0ACE8A00077F87 /* Products */; productRefGroup = EA836AC02F0ACE8A00077F87 /* Products */;
@ -325,7 +308,6 @@
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
@ -356,7 +338,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 6R7KLBPBLZ; DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
@ -380,7 +362,6 @@
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
}; };
@ -392,7 +373,6 @@
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
@ -423,7 +403,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 6R7KLBPBLZ; DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
@ -440,7 +420,6 @@
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_COMPILATION_MODE = wholemodule; SWIFT_COMPILATION_MODE = wholemodule;
VALIDATE_PRODUCT = YES; VALIDATE_PRODUCT = YES;
}; };
@ -453,7 +432,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = SelfieCam/SelfieCam.entitlements; CODE_SIGN_ENTITLEMENTS = SelfieCam/SelfieCam.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = NO; GENERATE_INFOPLIST_FILE = NO;
@ -463,9 +442,8 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.3; MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)"; PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
PRODUCT_MODULE_NAME = SelfieCam;
PRODUCT_NAME = "$(PRODUCT_NAME)"; PRODUCT_NAME = "$(PRODUCT_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES;
@ -484,7 +462,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = SelfieCam/SelfieCam.entitlements; CODE_SIGN_ENTITLEMENTS = SelfieCam/SelfieCam.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = NO; GENERATE_INFOPLIST_FILE = NO;
@ -494,9 +472,8 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.3; MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)"; PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
PRODUCT_MODULE_NAME = SelfieCam;
PRODUCT_NAME = "$(PRODUCT_NAME)"; PRODUCT_NAME = "$(PRODUCT_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES;
@ -513,20 +490,20 @@
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.0; IPHONEOS_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(TESTS_BUNDLE_IDENTIFIER)"; PRODUCT_BUNDLE_IDENTIFIER = "$(TESTS_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(PRODUCT_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO; STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Selfie Cam.app/Selfie Cam"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SelfieCam.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SelfieCam";
}; };
name = Debug; name = Debug;
}; };
@ -535,20 +512,20 @@
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.0; IPHONEOS_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(TESTS_BUNDLE_IDENTIFIER)"; PRODUCT_BUNDLE_IDENTIFIER = "$(TESTS_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(PRODUCT_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO; STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Selfie Cam.app/Selfie Cam"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SelfieCam.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SelfieCam";
}; };
name = Release; name = Release;
}; };
@ -556,12 +533,12 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(UITESTS_BUNDLE_IDENTIFIER)"; PRODUCT_BUNDLE_IDENTIFIER = "$(UITESTS_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(PRODUCT_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO; STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_EMIT_LOC_STRINGS = NO;
@ -576,12 +553,12 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(UITESTS_BUNDLE_IDENTIFIER)"; PRODUCT_BUNDLE_IDENTIFIER = "$(UITESTS_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(PRODUCT_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO; STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_EMIT_LOC_STRINGS = NO;
@ -634,9 +611,9 @@
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */ /* Begin XCLocalSwiftPackageReference section */
EA756A782F464B02006196BB /* XCLocalSwiftPackageReference "../_Packages/Bedrock" */ = { EA836AF72F0AD00000077F87 /* XCLocalSwiftPackageReference "../Bedrock" */ = {
isa = XCLocalSwiftPackageReference; isa = XCLocalSwiftPackageReference;
relativePath = ../_Packages/Bedrock; relativePath = ../Bedrock;
}; };
/* End XCLocalSwiftPackageReference section */ /* End XCLocalSwiftPackageReference section */
@ -660,10 +637,6 @@
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
EA756A792F464B02006196BB /* Bedrock */ = {
isa = XCSwiftPackageProductDependency;
productName = Bedrock;
};
EA836AEF2F0AD00000077F87 /* RevenueCat */ = { EA836AEF2F0AD00000077F87 /* RevenueCat */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = EA836AEE2F0AD00000077F87 /* XCRemoteSwiftPackageReference "purchases-ios-spm" */; package = EA836AEE2F0AD00000077F87 /* XCRemoteSwiftPackageReference "purchases-ios-spm" */;
@ -674,6 +647,11 @@
package = EA836AEE2F0AD00000077F87 /* XCRemoteSwiftPackageReference "purchases-ios-spm" */; package = EA836AEE2F0AD00000077F87 /* XCRemoteSwiftPackageReference "purchases-ios-spm" */;
productName = RevenueCatUI; productName = RevenueCatUI;
}; };
EA836AF32F0AD00000077F87 /* Bedrock */ = {
isa = XCSwiftPackageProductDependency;
package = EA836AF72F0AD00000077F87 /* XCLocalSwiftPackageReference "../Bedrock" */;
productName = Bedrock;
};
EA836AF52F0AD00000077F87 /* MijickCamera */ = { EA836AF52F0AD00000077F87 /* MijickCamera */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = EA836AF82F0AD00000077F87 /* XCRemoteSwiftPackageReference "MijickCamera" */; package = EA836AF82F0AD00000077F87 /* XCRemoteSwiftPackageReference "MijickCamera" */;

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "2630" LastUpgradeVersion = "2600"
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
@ -36,7 +36,7 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "EA836ACB2F0ACE8B00077F87" BlueprintIdentifier = "EA836ACB2F0ACE8B00077F87"
BuildableName = "SelfieCamTests.xctest" BuildableName = "Selfie Cam.xctest"
BlueprintName = "SelfieCamTests" BlueprintName = "SelfieCamTests"
ReferencedContainer = "container:SelfieCam.xcodeproj"> ReferencedContainer = "container:SelfieCam.xcodeproj">
</BuildableReference> </BuildableReference>
@ -47,7 +47,7 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "EA836AD52F0ACE8B00077F87" BlueprintIdentifier = "EA836AD52F0ACE8B00077F87"
BuildableName = "SelfieCamUITests.xctest" BuildableName = "Selfie Cam.xctest"
BlueprintName = "SelfieCamUITests" BlueprintName = "SelfieCamUITests"
ReferencedContainer = "container:SelfieCam.xcodeproj"> ReferencedContainer = "container:SelfieCam.xcodeproj">
</BuildableReference> </BuildableReference>

View File

@ -21,11 +21,10 @@ struct RootView: View {
/// Whether to show the paywall (shared between views) /// Whether to show the paywall (shared between views)
@State private var showPaywall = false @State private var showPaywall = false
private let isScreenshotUITest = ProcessInfo.processInfo.arguments.contains("-ui-testing-screenshots")
var body: some View { var body: some View {
ZStack { ZStack {
if isScreenshotUITest || hasCompletedOnboarding { if hasCompletedOnboarding {
// Main app content // Main app content
ContentView() ContentView()
.preferredColorScheme(.dark) .preferredColorScheme(.dark)

View File

@ -13,7 +13,6 @@ struct ContentView: View {
@State private var settings = SettingsViewModel() @State private var settings = SettingsViewModel()
@State private var showSettings = false @State private var showSettings = false
@State private var showPaywall = false @State private var showPaywall = false
private let isScreenshotUITest = ProcessInfo.processInfo.arguments.contains("-ui-testing-screenshots")
@State private var capturedPhoto: CapturedPhoto? @State private var capturedPhoto: CapturedPhoto?
@State private var showPhotoReview = false @State private var showPhotoReview = false
@ -33,25 +32,15 @@ struct ContentView: View {
ZStack { ZStack {
// Camera view - wrapped in EquatableView to prevent re-evaluation on settings changes // Camera view - wrapped in EquatableView to prevent re-evaluation on settings changes
if !showPhotoReview { if !showPhotoReview {
if isScreenshotUITest { EquatableView(content: CameraContainerView(
ScreenshotCameraPlaceholder( settings: settings,
showSettings: $showSettings, sessionKey: cameraSessionKey,
ringWidth: 70.0, cameraPosition: settings.cameraPosition,
ringColor: .white, onImageCaptured: { image in
ringOpacity: 0.7 handlePhotoCaptured(image)
) }
.ignoresSafeArea() ))
} else { .ignoresSafeArea() // Only camera ignores safe area to fill screen
EquatableView(content: CameraContainerView(
settings: settings,
sessionKey: cameraSessionKey,
cameraPosition: settings.cameraPosition,
onImageCaptured: { image in
handlePhotoCaptured(image)
}
))
.ignoresSafeArea() // Only camera ignores safe area to fill screen
}
} }
// Photo review overlay - handles its own safe area // Photo review overlay - handles its own safe area
@ -78,7 +67,7 @@ struct ContentView: View {
) )
// Settings button overlay - only show when NOT in photo review mode // Settings button overlay - only show when NOT in photo review mode
.overlay(alignment: .topTrailing) { .overlay(alignment: .topTrailing) {
if !showPhotoReview && !isScreenshotUITest { if !showPhotoReview {
Button { Button {
showSettings = true showSettings = true
} label: { } label: {
@ -94,13 +83,6 @@ struct ContentView: View {
.padding(.top, Design.Spacing.small) .padding(.top, Design.Spacing.small)
} }
} }
.overlay(alignment: .topLeading) {
if isScreenshotUITest {
Color.clear
.frame(width: 1, height: 1)
.accessibilityIdentifier("screenshot-mode-active")
}
}
.animation(.easeInOut(duration: Design.Animation.quick), value: showPhotoReview) .animation(.easeInOut(duration: Design.Animation.quick), value: showPhotoReview)
.onAppear { .onAppear {
// Initialize tracking of camera position // Initialize tracking of camera position
@ -238,3 +220,7 @@ struct CameraContainerView: View, Equatable {
.startSession() .startSession()
} }
} }
#Preview {
ContentView()
}

View File

@ -1,151 +0,0 @@
import SwiftUI
import Bedrock
struct ScreenshotCameraPlaceholder: View {
@Binding var showSettings: Bool
let ringWidth: CGFloat
let ringColor: Color
let ringOpacity: Double
init(
showSettings: Binding<Bool> = .constant(false),
ringWidth: CGFloat = 70,
ringColor: Color = .white,
ringOpacity: Double = 0.7
) {
self._showSettings = showSettings
self.ringWidth = ringWidth
self.ringColor = ringColor
self.ringOpacity = ringOpacity
}
private var backgroundImage: UIImage? {
guard let imageURL = Bundle.main.url(forResource: "image", withExtension: "png") else {
return nil
}
return UIImage(contentsOfFile: imageURL.path)
}
@ViewBuilder
private func cameraImageLayer(size: CGSize) -> some View {
if let backgroundImage {
Image(uiImage: backgroundImage)
.resizable()
.scaledToFill()
.frame(width: size.width, height: size.height, alignment: .center)
.clipped()
} else {
Color.black
.frame(width: size.width, height: size.height)
}
}
var body: some View {
GeometryReader { geometry in
let size = geometry.size
ZStack {
cameraImageLayer(size: size)
ringColor
.opacity(ringOpacity)
.ignoresSafeArea()
cameraImageLayer(size: size)
.mask {
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
.padding(ringWidth)
}
PlaceholderGridOverlay(ringWidth: ringWidth)
.allowsHitTesting(false)
VStack(spacing: 0) {
HStack {
Spacer()
PlaceholderTopButton(
systemImage: "gearshape.fill",
accessibilityLabel: "Settings",
action: { showSettings = true }
)
}
.padding(.top, ringWidth + Design.Spacing.medium)
.padding(.trailing, Design.Spacing.medium)
Spacer()
VStack(spacing: Design.Spacing.medium) {
ZoomControlView(
zoomFactor: 1.0,
isCenterStageActive: false
)
CaptureButton(action: { })
.padding(.bottom, Design.Spacing.large)
}
}
}
}
.ignoresSafeArea()
.accessibilityIdentifier("screenshot-camera-placeholder")
.overlay(alignment: .topLeading) {
Color.clear
.frame(width: 1, height: 1)
.accessibilityIdentifier("screenshot-camera-placeholder")
}
}
}
private struct PlaceholderTopButton: View {
let systemImage: String
let accessibilityLabel: String
let action: () -> Void
var body: some View {
Button(action: action) {
Image(systemName: systemImage)
.font(.title3)
.foregroundStyle(.white)
.frame(width: 44, height: 44)
.background(Color.black.opacity(Design.Opacity.medium), in: Circle())
.overlay {
Circle()
.strokeBorder(Color.white.opacity(Design.Opacity.subtle), lineWidth: Design.LineWidth.thin)
}
.shadow(radius: Design.Shadow.radiusSmall)
}
.accessibilityLabel(accessibilityLabel)
}
}
private struct PlaceholderGridOverlay: View {
let ringWidth: CGFloat
var body: some View {
GeometryReader { geometry in
let insetRect = CGRect(
x: ringWidth,
y: ringWidth,
width: geometry.size.width - (ringWidth * 2),
height: geometry.size.height - (ringWidth * 2)
)
Path { path in
for index in 1...2 {
let x = insetRect.minX + (insetRect.width * CGFloat(index) / 3)
path.move(to: CGPoint(x: x, y: insetRect.minY))
path.addLine(to: CGPoint(x: x, y: insetRect.maxY))
}
for index in 1...2 {
let y = insetRect.minY + (insetRect.height * CGFloat(index) / 3)
path.move(to: CGPoint(x: insetRect.minX, y: y))
path.addLine(to: CGPoint(x: insetRect.maxX, y: y))
}
}
.stroke(Color.white.opacity(Design.Opacity.subtle), lineWidth: Design.LineWidth.thin)
}
}
}
#Preview {
ScreenshotCameraPlaceholder(ringWidth: 70.0, ringColor: .white, ringOpacity: 0.7)
}

View File

@ -45,10 +45,12 @@ enum OnboardingPermissionType {
} }
} }
/// Neutral progression wording for pre-permission prompts.
/// Apple review guidance recommends labels like "Continue" or "Next".
var buttonTitle: String { var buttonTitle: String {
String(localized: "Continue") switch self {
case .camera: return String(localized: "Enable Camera")
case .microphone: return String(localized: "Enable Microphone")
case .photoLibrary: return String(localized: "Enable Photos")
}
} }
var deniedTitle: String { var deniedTitle: String {

View File

@ -25,9 +25,6 @@ struct ProPaywallView: View {
/// Currently selected package /// Currently selected package
@State private var selectedPackage: Package? @State private var selectedPackage: Package?
/// Whether products are loading from RevenueCat
@State private var isLoadingProducts = false
var body: some View { var body: some View {
NavigationStack { NavigationStack {
ScrollView { ScrollView {
@ -52,7 +49,10 @@ struct ProPaywallView: View {
} }
.font(.system(size: bodyFontSize)) .font(.system(size: bodyFontSize))
.task { .task {
await loadProducts() try? await manager.loadProducts()
if selectedPackage == nil {
selectedPackage = preferredPackage(from: manager.availablePackages)
}
} }
.onChange(of: manager.availablePackages) { _, newValue in .onChange(of: manager.availablePackages) { _, newValue in
if selectedPackage == nil { if selectedPackage == nil {
@ -117,25 +117,9 @@ struct ProPaywallView: View {
private var packageSelection: some View { private var packageSelection: some View {
VStack(spacing: Design.Spacing.medium) { VStack(spacing: Design.Spacing.medium) {
if isLoadingProducts { if manager.availablePackages.isEmpty {
ProgressView() ProgressView()
.padding() .padding()
} else if manager.availablePackages.isEmpty {
VStack(spacing: Design.Spacing.small) {
Text(String(localized: "Plans are currently unavailable. Please try again."))
.font(.caption)
.foregroundStyle(.white.opacity(Design.Opacity.medium))
.multilineTextAlignment(.center)
Button(String(localized: "Retry")) {
Task {
await loadProducts()
}
}
.font(.footnote.weight(.semibold))
.foregroundStyle(AppAccent.primary)
}
.padding(.vertical, Design.Spacing.medium)
} else { } else {
ForEach(manager.availablePackages, id: \.identifier) { package in ForEach(manager.availablePackages, id: \.identifier) { package in
PackageOptionRow( PackageOptionRow(
@ -152,15 +136,6 @@ struct ProPaywallView: View {
private var purchaseCTA: some View { private var purchaseCTA: some View {
VStack(spacing: Design.Spacing.small) { VStack(spacing: Design.Spacing.small) {
Button { Button {
guard !manager.availablePackages.isEmpty else {
errorMessage = String(localized: "Plans are unavailable right now. Please try again.")
showError = true
Task {
await loadProducts()
}
return
}
guard let selectedPackage else { guard let selectedPackage else {
errorMessage = String(localized: "Please select a plan.") errorMessage = String(localized: "Please select a plan.")
showError = true showError = true
@ -175,12 +150,7 @@ struct ProPaywallView: View {
ProgressView() ProgressView()
.tint(.white) .tint(.white)
} }
Text(String(localized: "Continue"))
Text(
isPurchasing
? String(localized: "Processing...")
: String(localized: "Continue")
)
.font(.headline) .font(.headline)
.foregroundStyle(.white) .foregroundStyle(.white)
} }
@ -189,8 +159,7 @@ struct ProPaywallView: View {
.background(AppAccent.primary) .background(AppAccent.primary)
.clipShape(.rect(cornerRadius: Design.CornerRadius.large)) .clipShape(.rect(cornerRadius: Design.CornerRadius.large))
} }
.disabled(isPurchasing || isRestoring || isLoadingProducts) .disabled(isPurchasing || isRestoring || selectedPackage == nil)
.opacity((isPurchasing || isRestoring || isLoadingProducts) ? 0.7 : 1.0)
Text(String(localized: "Cancel anytime. Payment will be charged to your Apple ID.")) Text(String(localized: "Cancel anytime. Payment will be charged to your Apple ID."))
.font(.caption) .font(.caption)
@ -269,24 +238,6 @@ struct ProPaywallView: View {
// MARK: - Helpers // MARK: - Helpers
private func loadProducts() async {
guard !isLoadingProducts else { return }
isLoadingProducts = true
defer { isLoadingProducts = false }
do {
try await manager.loadProducts()
if selectedPackage == nil {
selectedPackage = preferredPackage(from: manager.availablePackages)
}
} catch {
#if DEBUG
print("❌ [ProPaywallView] Failed to load products: \(error.localizedDescription)")
#endif
}
}
private func preferredPackage(from packages: [Package]) -> Package? { private func preferredPackage(from packages: [Package]) -> Package? {
if let annual = packages.first(where: { $0.packageType == .annual }) { if let annual = packages.first(where: { $0.packageType == .annual }) {
return annual return annual

View File

@ -203,7 +203,6 @@ struct SettingsView: View {
} }
} }
} }
.accessibilityIdentifier("settings-sheet")
} }
// MARK: - Ring Size Slider // MARK: - Ring Size Slider

View File

@ -997,7 +997,6 @@
}, },
"Debug mode: Purchase simulated!" : { "Debug mode: Purchase simulated!" : {
"comment" : "Announcement posted to VoiceOver when a premium purchase is simulated in debug mode.", "comment" : "Announcement posted to VoiceOver when a premium purchase is simulated in debug mode.",
"extractionState" : "stale",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
"localizations" : { "localizations" : {
"es-MX" : { "es-MX" : {
@ -1022,7 +1021,6 @@
}, },
"Debug mode: Restore simulated!" : { "Debug mode: Restore simulated!" : {
"comment" : "Accessibility announcement when restoring purchases in debug mode.", "comment" : "Accessibility announcement when restoring purchases in debug mode.",
"extractionState" : "stale",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
"localizations" : { "localizations" : {
"es-MX" : { "es-MX" : {
@ -1125,6 +1123,10 @@
"comment" : "Subtitle for a feature row in the \"Welcome\" view, describing the ease of capturing photos.", "comment" : "Subtitle for a feature row in the \"Welcome\" view, describing the ease of capturing photos.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Enable Camera" : {
"comment" : "Text on the action button for enabling camera permission.",
"isCommentAutoGenerated" : true
},
"Enable Center Stage" : { "Enable Center Stage" : {
"comment" : "An accessibility label for the toggle that enables the \"Center Stage\" feature.", "comment" : "An accessibility label for the toggle that enables the \"Center Stage\" feature.",
"extractionState" : "stale", "extractionState" : "stale",
@ -1150,6 +1152,14 @@
} }
} }
}, },
"Enable Microphone" : {
"comment" : "Title for the button that enables microphone access.",
"isCommentAutoGenerated" : true
},
"Enable Photos" : {
"comment" : "Title for the button that enables photo library access in the onboarding.",
"isCommentAutoGenerated" : true
},
"Enable Ring Light" : { "Enable Ring Light" : {
"comment" : "Title of a toggle in the Settings view that allows the user to enable or disable the ring light overlay.", "comment" : "Title of a toggle in the Settings view that allows the user to enable or disable the ring light overlay.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
@ -1798,6 +1808,14 @@
} }
} }
}, },
"Onboarding Reset" : {
"comment" : "The title of an alert that confirms the onboarding state has been reset.",
"isCommentAutoGenerated" : true
},
"Onboarding will show again when you restart the app." : {
"comment" : "A message displayed in an alert when the \"Reset Onboarding\" button is tapped.",
"isCommentAutoGenerated" : true
},
"One Time" : { "One Time" : {
"comment" : "A description of a one-time purchase option.", "comment" : "A description of a one-time purchase option.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@ -1948,14 +1966,6 @@
} }
} }
}, },
"Plans are currently unavailable. Please try again." : {
"comment" : "A message displayed when the user is unable to load available subscription plans.",
"isCommentAutoGenerated" : true
},
"Plans are unavailable right now. Please try again." : {
"comment" : "An error message displayed when the user tries to purchase a plan but there are no available plans.",
"isCommentAutoGenerated" : true
},
"Please select a plan." : { "Please select a plan." : {
"comment" : "Error message displayed when the user tries to purchase a package but hasn't selected one.", "comment" : "Error message displayed when the user tries to purchase a package but hasn't selected one.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@ -2047,10 +2057,6 @@
"comment" : "An accessibility label for the section of the settings view that displays that their Pro subscription is active.", "comment" : "An accessibility label for the section of the settings view that displays that their Pro subscription is active.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Processing..." : {
"comment" : "A label displayed while a purchase is being processed.",
"isCommentAutoGenerated" : true
},
"Purchase Error" : { "Purchase Error" : {
"comment" : "The title of an alert that appears when there is an error during a purchase process.", "comment" : "The title of an alert that appears when there is an error during a purchase process.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@ -2127,6 +2133,10 @@
} }
} }
}, },
"Reset Onboarding" : {
"comment" : "A button label that resets the onboarding state.",
"isCommentAutoGenerated" : true
},
"Restore Purchases" : { "Restore Purchases" : {
"comment" : "A button that restores purchases.", "comment" : "A button that restores purchases.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
@ -2200,10 +2210,6 @@
} }
} }
}, },
"Retry" : {
"comment" : "A button label that attempts to reload available products when the initial load fails.",
"isCommentAutoGenerated" : true
},
"Ring Light" : { "Ring Light" : {
"localizations" : { "localizations" : {
"es-MX" : { "es-MX" : {
@ -2781,6 +2787,10 @@
} }
} }
}, },
"Show onboarding flow on next app launch" : {
"comment" : "A description of what the \"Reset Onboarding\" button does.",
"isCommentAutoGenerated" : true
},
"Shows a grid overlay to help compose your shot" : { "Shows a grid overlay to help compose your shot" : {
"comment" : "A toggle that enables or disables the rule of thirds grid overlay in the camera view.", "comment" : "A toggle that enables or disables the rule of thirds grid overlay in the camera view.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

View File

@ -0,0 +1,44 @@
//
// Color+Codable.swift
// CameraTester
//
// Created by Matt Bruce on 1/3/26.
//
import SwiftUI
// MARK: - Color Codable Extension
extension Color: Codable {
enum CodingKeys: String, CodingKey {
case red, green, blue, opacity
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let red = try container.decode(Double.self, forKey: .red)
let green = try container.decode(Double.self, forKey: .green)
let blue = try container.decode(Double.self, forKey: .blue)
let opacity = try container.decode(Double.self, forKey: .opacity)
self.init(red: red, green: green, blue: blue, opacity: opacity)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
// Convert Color to RGB components
let uiColor = UIColor(self)
var red: CGFloat = 0
var green: CGFloat = 0
var blue: CGFloat = 0
var alpha: CGFloat = 0
uiColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
try container.encode(Double(red), forKey: .red)
try container.encode(Double(green), forKey: .green)
try container.encode(Double(blue), forKey: .blue)
try container.encode(Double(alpha), forKey: .opacity)
}
}

View File

@ -11,22 +11,17 @@ import SwiftUI
struct CameraSettings: Codable { struct CameraSettings: Codable {
var photoQuality: PhotoQuality var photoQuality: PhotoQuality
var isRingLightEnabled: Bool var isRingLightEnabled: Bool
var ringLightColorRGB: CustomColorRGB var ringLightColor: Color
var ringLightSize: CGFloat var ringLightSize: CGFloat
var ringLightOpacity: Double var ringLightOpacity: Double
var flashMode: CameraFlashMode var flashMode: CameraFlashMode
var isFlashSyncedWithRingLight: Bool var isFlashSyncedWithRingLight: Bool
var ringLightColor: Color {
get { ringLightColorRGB.color }
set { ringLightColorRGB = CustomColorRGB(from: newValue) }
}
// Default settings // Default settings
static let `default` = CameraSettings( static let `default` = CameraSettings(
photoQuality: .high, photoQuality: .high,
isRingLightEnabled: true, isRingLightEnabled: true,
ringLightColorRGB: .defaultWhite, ringLightColor: .white,
ringLightSize: 25, ringLightSize: 25,
ringLightOpacity: 1.0, ringLightOpacity: 1.0,
flashMode: .off, flashMode: .off,

View File

@ -8,123 +8,34 @@
import XCTest import XCTest
final class SelfieCamUITests: XCTestCase { final class SelfieCamUITests: XCTestCase {
private var app: XCUIApplication!
override func setUpWithError() throws { override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false continueAfterFailure = false
app = XCUIApplication()
addUIInterruptionMonitor(withDescription: "System Permission Alerts") { alert in
let preferredButtons = [
"Allow",
"Allow While Using App",
"OK",
"Continue"
]
for title in preferredButtons where alert.buttons[title].exists { // In UI tests its important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
alert.buttons[title].tap()
return true
}
if let firstButton = alert.buttons.allElementsBoundByIndex.first {
firstButton.tap()
return true
}
return false
}
} }
override func tearDownWithError() throws { override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
} }
@MainActor @MainActor
func testAppStorePortraitScreenshots() throws { func testExample() throws {
app.launchArguments = ["-ui-testing-screenshots"] // UI tests must launch the application that they test.
app.launchEnvironment = [ let app = XCUIApplication()
"ENABLE_DEBUG_PREMIUM": "1"
]
app.launch() app.launch()
if !waitForCameraScreen(timeout: 8) {
app.terminate()
app.launch()
}
saveScreenshot(named: "01-launch-screen") // Use XCTAssert and related functions to verify your tests produce the correct results.
XCTAssertTrue(waitForCameraScreen(timeout: 25), "Camera screen did not appear")
XCTAssertTrue(app.otherElements["screenshot-camera-placeholder"].waitForExistence(timeout: 5), "Screenshot camera placeholder did not appear")
RunLoop.current.run(until: Date().addingTimeInterval(0.6))
saveScreenshot(named: "02-camera-ring-light")
let settingsButton = app.buttons["Settings"]
XCTAssertTrue(settingsButton.waitForExistence(timeout: 10), "Settings button not found")
settingsButton.tap()
XCTAssertTrue(app.otherElements["settings-sheet"].waitForExistence(timeout: 8), "Settings sheet did not appear")
RunLoop.current.run(until: Date().addingTimeInterval(0.8))
saveScreenshot(named: "03-settings-popup")
} }
@MainActor @MainActor
func testLaunchPerformance() throws { func testLaunchPerformance() throws {
// This measures how long it takes to launch your application.
measure(metrics: [XCTApplicationLaunchMetric()]) { measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch() XCUIApplication().launch()
} }
} }
// MARK: - Helpers
@MainActor
private func waitForCameraScreen(timeout: TimeInterval) -> Bool {
let onboardingActions = [
"Get Started",
"Continue",
"Next",
"Done"
]
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
if app.otherElements["screenshot-mode-active"].exists {
return true
}
_ = tapFirstExistingButton(labels: onboardingActions)
tapFirstSpringboardButton(labels: ["Allow", "Allow While Using App", "OK", "Continue"])
if app.alerts.firstMatch.exists {
app.tap()
}
RunLoop.current.run(until: Date().addingTimeInterval(0.2))
}
return app.otherElements["screenshot-mode-active"].exists
}
@MainActor
private func tapFirstSpringboardButton(labels: [String]) {
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
for label in labels {
let button = springboard.buttons[label]
if button.waitForExistence(timeout: 0.2) {
button.tap()
return
}
}
}
@MainActor
private func tapFirstExistingButton(labels: [String]) -> Bool {
for label in labels {
let button = app.buttons[label]
if button.exists {
button.tap()
return true
}
}
return false
}
@MainActor
private func saveScreenshot(named name: String) {
let attachment = XCTAttachment(screenshot: app.screenshot())
attachment.name = name
attachment.lifetime = .keepAlways
add(attachment)
}
} }

View File

@ -21,6 +21,13 @@ final class SelfieCamUITestsLaunchTests: XCTestCase {
func testLaunch() throws { func testLaunch() throws {
let app = XCUIApplication() let app = XCUIApplication()
app.launch() app.launch()
XCTAssertTrue(app.state == .runningForeground)
// Insert steps here to perform after app launch but before taking a screenshot,
// such as logging into a test account or navigating somewhere in the app
let attachment = XCTAttachment(screenshot: app.screenshot())
attachment.name = "Launch Screen"
attachment.lifetime = .keepAlways
add(attachment)
} }
} }

View File

@ -1,80 +0,0 @@
# App Review Rejection Checklist
Last updated: 2026-02-14
## Current Status
- [x] Investigated rejection reasons and mapped each to implementation/App Store Connect actions.
- [x] Fixed paywall "Continue" no-op behavior when products are unavailable.
- File: `SelfieCam/Features/Paywall/Views/ProPaywallView.swift`
- Added product loading state, retry UI, and explicit error handling path.
- [x] Verified app still builds after paywall fix.
- Debug build: success
- Release build: success
- [x] Resolved UITest build blocker (`Multiple commands produce ... Selfie_Cam.swiftmodule`).
- File: `SelfieCam/SelfieCam.xcodeproj/project.pbxproj`
- Fix: set test target `PRODUCT_NAME = "$(TARGET_NAME)"` and app target `PRODUCT_MODULE_NAME = SelfieCam`.
- Validation: `xcodebuild ... build-for-testing` on iPad Air 11-inch (M3), iOS 26.2 succeeded.
## Guideline 2.1 - App Completeness (IAP not submitted)
- [x] In App Store Connect, ensure all IAPs are fully configured (metadata, pricing, localization).
- Pro Monthly, Pro Yearly (subscriptions), Pro Lifetime (non-consumable) all Ready to Submit.
- Product ID for lifetime: `com.mbrucedogs.SelfieCam.pro.lifetime.purchase`
- [x] Upload required App Review screenshot for each IAP/subscription.
- [ ] Submit all IAP products for review with the app version.
- [ ] Upload a new app binary and submit app + IAPs together.
## Guideline 2.1 - App Completeness (IAP bug: Continue unresponsive)
- [x] Code-side paywall resiliency fix implemented (no silent button failure).
- [x] Run sandbox purchase tests on iPad form factor before resubmission.
- Review device reported by Apple: iPad Air 11-inch (M3), iPadOS 26.2.1
- [x] Confirm Paid Apps Agreement is active.
- Location: App Store Connect -> Agreements, Tax, and Banking
## Guideline 1.5 - Safety (Support URL)
- [x] Update Support URL destination so it clearly provides support contact/help content.
- Current URL: `https://topdoglabs.com/support`
- [x] Ensure support page includes:
- Contact method (email or form)
- App name(s) supported
- Expected response time
- Troubleshooting/help info
- Link to privacy policy
- [x] Re-verify URL loads reliably in browser without requiring app login.
- Verified on 2026-02-12 via direct HTTP fetch: static HTML now includes support email, response time, privacy link, app-specific subject options, and contact details.
## Guideline 5.1.1 - Privacy (Permission Request Button Wording)
Issue reported: pre-permission custom screen uses "Enable Camera" style button text.
- [x] Replace pre-permission button titles with neutral progression text like "Continue" or "Next".
- File: `SelfieCam/Features/Onboarding/Views/OnboardingPermissionView.swift`
- Implemented: pre-permission button now uses localized `"Continue"` across camera/microphone/photo library prompts.
- [x] Update localized strings accordingly in string catalog.
- File: `SelfieCam/Resources/Localizable.xcstrings`
- Existing localized `"Continue"` key is already present for supported locales (`en`, `es-MX`, `fr-CA`), so no new key entries were required.
- [x] Update UI tests that currently look for old button labels.
- File: `SelfieCamUITests/SelfieCamUITests.swift`
- [x] Verify onboarding permission flow still works for camera/microphone/photo library.
- Validation run on 2026-02-14:
- `xcodebuild -project SelfieCam/SelfieCam.xcodeproj -scheme SelfieCam -destination 'platform=iOS Simulator,name=iPad Air 11-inch (M3),OS=26.2' test -only-testing:SelfieCamUITests/SelfieCamUITests/testAppStorePortraitScreenshots`
- Result: `** TEST SUCCEEDED **`
## Resubmission Checklist
- [x] Increment build number and archive a new build.
- Build number: `2` (`CURRENT_PROJECT_VERSION = 2`)
- Archive command:
- `xcodebuild -project SelfieCam/SelfieCam.xcodeproj -scheme SelfieCam -configuration Release -destination 'generic/platform=iOS' -archivePath /tmp/SelfieCam_2026-02-14.xcarchive CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION=YES archive`
- Result: `** ARCHIVE SUCCEEDED **`
- [ ] Upload new build to App Store Connect.
- [ ] Attach submitted IAPs to the app version.
- [ ] Add App Review Notes summarizing fixes:
- Paywall button behavior fixed
- IAPs submitted with required metadata/screenshots
- Support URL updated
- Permission request button wording updated
- [ ] Submit for review.