Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d2d4e3757 | |||
| d39b85603b | |||
| 21aade9ea1 | |||
| 20dd4db710 | |||
| 69545b55bc | |||
| 660974ff8f | |||
| 0ed1eee242 | |||
| 2f85c334cb | |||
| be3131ad7c | |||
| 9a0a48d889 | |||
| ec61a76602 | |||
| 772cb6e437 |
41
APP_STORE_CONNECT.md
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# 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
|
||||||
BIN
Screenshots/iPad-13/01-launch-screen.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
Screenshots/iPad-13/02-camera-ring-light.png
Normal file
|
After Width: | Height: | Size: 5.0 MiB |
BIN
Screenshots/iPad-13/03-settings-popup.png
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
BIN
Screenshots/iPhone-6.3/01-launch-screen.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
Screenshots/iPhone-6.3/02-camera-ring-light.png
Normal file
|
After Width: | Height: | Size: 2.9 MiB |
BIN
Screenshots/iPhone-6.3/03-settings-popup.png
Normal file
|
After Width: | Height: | Size: 317 KiB |
BIN
Screenshots/iPhone-6.5/01-launch-screen.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
Screenshots/iPhone-6.5/02-camera-ring-light.png
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
BIN
Screenshots/iPhone-6.5/03-settings-popup.png
Normal file
|
After Width: | Height: | Size: 309 KiB |
BIN
Screenshots/iPhone-6.9/01-launch-screen.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
Screenshots/iPhone-6.9/02-camera-ring-light.png
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
Screenshots/iPhone-6.9/03-settings-popup.png
Normal file
|
After Width: | Height: | Size: 352 KiB |
17
SelfieCam.code-workspace
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../_Packages/Bedrock"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"terminal.integrated.enablePersistentSessions": true,
|
||||||
|
"terminal.integrated.persistentSessionReviveProcess": "onExitAndWindowClose",
|
||||||
|
"task.allowAutomaticTasks": "off",
|
||||||
|
"swift.disableAutoResolve": true,
|
||||||
|
"swift.disableSwiftPackageManagerIntegration": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,15 +32,32 @@
|
|||||||
|
|
||||||
/* 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 /* Selfie Cam.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Selfie Cam.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
EA836ACC2F0ACE8B00077F87 /* SelfieCamTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SelfieCamTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
EA836AD62F0ACE8B00077F87 /* 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; };
|
||||||
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>";
|
||||||
};
|
};
|
||||||
@ -62,7 +79,7 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
EA836AF62F0AD00000077F87 /* MijickCamera in Frameworks */,
|
EA836AF62F0AD00000077F87 /* MijickCamera in Frameworks */,
|
||||||
EA836AF42F0AD00000077F87 /* Bedrock in Frameworks */,
|
EA756A7A2F464B02006196BB /* Bedrock in Frameworks */,
|
||||||
EA836AF02F0AD00000077F87 /* RevenueCat in Frameworks */,
|
EA836AF02F0AD00000077F87 /* RevenueCat in Frameworks */,
|
||||||
EA836AF22F0AD00000077F87 /* RevenueCatUI in Frameworks */,
|
EA836AF22F0AD00000077F87 /* RevenueCatUI in Frameworks */,
|
||||||
);
|
);
|
||||||
@ -100,8 +117,8 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
EA836ABF2F0ACE8A00077F87 /* Selfie Cam.app */,
|
EA836ABF2F0ACE8A00077F87 /* Selfie Cam.app */,
|
||||||
EA836ACC2F0ACE8B00077F87 /* Selfie Cam.xctest */,
|
EA836ACC2F0ACE8B00077F87 /* SelfieCamTests.xctest */,
|
||||||
EA836AD62F0ACE8B00077F87 /* Selfie Cam.xctest */,
|
EA836AD62F0ACE8B00077F87 /* SelfieCamUITests.xctest */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -137,8 +154,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 */;
|
||||||
@ -164,7 +181,7 @@
|
|||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
);
|
);
|
||||||
productName = SelfieCamTests;
|
productName = SelfieCamTests;
|
||||||
productReference = EA836ACC2F0ACE8B00077F87 /* Selfie Cam.xctest */;
|
productReference = EA836ACC2F0ACE8B00077F87 /* SelfieCamTests.xctest */;
|
||||||
productType = "com.apple.product-type.bundle.unit-test";
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
};
|
};
|
||||||
EA836AD52F0ACE8B00077F87 /* SelfieCamUITests */ = {
|
EA836AD52F0ACE8B00077F87 /* SelfieCamUITests */ = {
|
||||||
@ -187,7 +204,7 @@
|
|||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
);
|
);
|
||||||
productName = SelfieCamUITests;
|
productName = SelfieCamUITests;
|
||||||
productReference = EA836AD62F0ACE8B00077F87 /* Selfie Cam.xctest */;
|
productReference = EA836AD62F0ACE8B00077F87 /* SelfieCamUITests.xctest */;
|
||||||
productType = "com.apple.product-type.bundle.ui-testing";
|
productType = "com.apple.product-type.bundle.ui-testing";
|
||||||
};
|
};
|
||||||
/* End PBXNativeTarget section */
|
/* End PBXNativeTarget section */
|
||||||
@ -198,7 +215,7 @@
|
|||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = 1;
|
BuildIndependentTargetsInParallel = 1;
|
||||||
LastSwiftUpdateCheck = 2600;
|
LastSwiftUpdateCheck = 2600;
|
||||||
LastUpgradeCheck = 2600;
|
LastUpgradeCheck = 2630;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
EA836ABE2F0ACE8A00077F87 = {
|
EA836ABE2F0ACE8A00077F87 = {
|
||||||
CreatedOnToolsVersion = 26.0;
|
CreatedOnToolsVersion = 26.0;
|
||||||
@ -225,8 +242,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 */;
|
||||||
@ -308,6 +325,7 @@
|
|||||||
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";
|
||||||
@ -338,7 +356,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 = "$(DEVELOPMENT_TEAM)";
|
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
|
||||||
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;
|
||||||
@ -362,6 +380,7 @@
|
|||||||
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";
|
||||||
};
|
};
|
||||||
@ -373,6 +392,7 @@
|
|||||||
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";
|
||||||
@ -403,7 +423,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 = "$(DEVELOPMENT_TEAM)";
|
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
|
||||||
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;
|
||||||
@ -420,6 +440,7 @@
|
|||||||
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;
|
||||||
};
|
};
|
||||||
@ -432,7 +453,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 = 1;
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
|
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
@ -442,8 +463,9 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.2;
|
MARKETING_VERSION = 1.3;
|
||||||
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;
|
||||||
@ -462,7 +484,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 = 1;
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
|
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
@ -472,8 +494,9 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.2;
|
MARKETING_VERSION = 1.3;
|
||||||
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;
|
||||||
@ -490,20 +513,20 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
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 = "$(PRODUCT_NAME)";
|
PRODUCT_NAME = "$(TARGET_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)/SelfieCam.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SelfieCam";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Selfie Cam.app/Selfie Cam";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
@ -512,20 +535,20 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
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 = "$(PRODUCT_NAME)";
|
PRODUCT_NAME = "$(TARGET_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)/SelfieCam.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SelfieCam";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Selfie Cam.app/Selfie Cam";
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
@ -533,12 +556,12 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
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 = "$(PRODUCT_NAME)";
|
PRODUCT_NAME = "$(TARGET_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;
|
||||||
@ -553,12 +576,12 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
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 = "$(PRODUCT_NAME)";
|
PRODUCT_NAME = "$(TARGET_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;
|
||||||
@ -611,9 +634,9 @@
|
|||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCLocalSwiftPackageReference section */
|
/* Begin XCLocalSwiftPackageReference section */
|
||||||
EA836AF72F0AD00000077F87 /* XCLocalSwiftPackageReference "../Bedrock" */ = {
|
EA756A782F464B02006196BB /* XCLocalSwiftPackageReference "../_Packages/Bedrock" */ = {
|
||||||
isa = XCLocalSwiftPackageReference;
|
isa = XCLocalSwiftPackageReference;
|
||||||
relativePath = ../Bedrock;
|
relativePath = ../_Packages/Bedrock;
|
||||||
};
|
};
|
||||||
/* End XCLocalSwiftPackageReference section */
|
/* End XCLocalSwiftPackageReference section */
|
||||||
|
|
||||||
@ -637,6 +660,10 @@
|
|||||||
/* 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" */;
|
||||||
@ -647,11 +674,6 @@
|
|||||||
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" */;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "2600"
|
LastUpgradeVersion = "2630"
|
||||||
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 = "Selfie Cam.xctest"
|
BuildableName = "SelfieCamTests.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 = "Selfie Cam.xctest"
|
BuildableName = "SelfieCamUITests.xctest"
|
||||||
BlueprintName = "SelfieCamUITests"
|
BlueprintName = "SelfieCamUITests"
|
||||||
ReferencedContainer = "container:SelfieCam.xcodeproj">
|
ReferencedContainer = "container:SelfieCam.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
|
|||||||
@ -21,10 +21,11 @@ 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 hasCompletedOnboarding {
|
if isScreenshotUITest || hasCompletedOnboarding {
|
||||||
// Main app content
|
// Main app content
|
||||||
ContentView()
|
ContentView()
|
||||||
.preferredColorScheme(.dark)
|
.preferredColorScheme(.dark)
|
||||||
|
|||||||
@ -13,6 +13,7 @@ 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
|
||||||
@ -32,15 +33,25 @@ 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 {
|
||||||
EquatableView(content: CameraContainerView(
|
if isScreenshotUITest {
|
||||||
settings: settings,
|
ScreenshotCameraPlaceholder(
|
||||||
sessionKey: cameraSessionKey,
|
showSettings: $showSettings,
|
||||||
cameraPosition: settings.cameraPosition,
|
ringWidth: 70.0,
|
||||||
onImageCaptured: { image in
|
ringColor: .white,
|
||||||
handlePhotoCaptured(image)
|
ringOpacity: 0.7
|
||||||
}
|
)
|
||||||
))
|
.ignoresSafeArea()
|
||||||
.ignoresSafeArea() // Only camera ignores safe area to fill screen
|
} else {
|
||||||
|
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
|
||||||
@ -67,7 +78,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 {
|
if !showPhotoReview && !isScreenshotUITest {
|
||||||
Button {
|
Button {
|
||||||
showSettings = true
|
showSettings = true
|
||||||
} label: {
|
} label: {
|
||||||
@ -83,6 +94,13 @@ 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
|
||||||
@ -220,7 +238,3 @@ struct CameraContainerView: View, Equatable {
|
|||||||
.startSession()
|
.startSession()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
|
||||||
ContentView()
|
|
||||||
}
|
|
||||||
|
|||||||
@ -0,0 +1,151 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@ -45,12 +45,10 @@ enum OnboardingPermissionType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Neutral progression wording for pre-permission prompts.
|
||||||
|
/// Apple review guidance recommends labels like "Continue" or "Next".
|
||||||
var buttonTitle: String {
|
var buttonTitle: String {
|
||||||
switch self {
|
String(localized: "Continue")
|
||||||
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 {
|
||||||
|
|||||||
@ -25,6 +25,9 @@ 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 {
|
||||||
@ -49,10 +52,7 @@ struct ProPaywallView: View {
|
|||||||
}
|
}
|
||||||
.font(.system(size: bodyFontSize))
|
.font(.system(size: bodyFontSize))
|
||||||
.task {
|
.task {
|
||||||
try? await manager.loadProducts()
|
await 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,9 +117,25 @@ struct ProPaywallView: View {
|
|||||||
|
|
||||||
private var packageSelection: some View {
|
private var packageSelection: some View {
|
||||||
VStack(spacing: Design.Spacing.medium) {
|
VStack(spacing: Design.Spacing.medium) {
|
||||||
if manager.availablePackages.isEmpty {
|
if isLoadingProducts {
|
||||||
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(
|
||||||
@ -136,6 +152,15 @@ 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
|
||||||
@ -150,7 +175,12 @@ 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)
|
||||||
}
|
}
|
||||||
@ -159,7 +189,8 @@ struct ProPaywallView: View {
|
|||||||
.background(AppAccent.primary)
|
.background(AppAccent.primary)
|
||||||
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
|
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
|
||||||
}
|
}
|
||||||
.disabled(isPurchasing || isRestoring || selectedPackage == nil)
|
.disabled(isPurchasing || isRestoring || isLoadingProducts)
|
||||||
|
.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)
|
||||||
@ -238,6 +269,24 @@ 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
|
||||||
|
|||||||
@ -203,6 +203,7 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.accessibilityIdentifier("settings-sheet")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Ring Size Slider
|
// MARK: - Ring Size Slider
|
||||||
|
|||||||
@ -997,6 +997,7 @@
|
|||||||
},
|
},
|
||||||
"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" : {
|
||||||
@ -1021,6 +1022,7 @@
|
|||||||
},
|
},
|
||||||
"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" : {
|
||||||
@ -1123,10 +1125,6 @@
|
|||||||
"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",
|
||||||
@ -1152,14 +1150,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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,
|
||||||
@ -1808,14 +1798,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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
|
||||||
@ -1966,6 +1948,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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
|
||||||
@ -2057,6 +2047,10 @@
|
|||||||
"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
|
||||||
@ -2133,10 +2127,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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,
|
||||||
@ -2210,6 +2200,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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" : {
|
||||||
@ -2787,10 +2781,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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,
|
||||||
|
|||||||
BIN
SelfieCam/Resources/image.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
@ -1,44 +0,0 @@
|
|||||||
//
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -11,17 +11,22 @@ import SwiftUI
|
|||||||
struct CameraSettings: Codable {
|
struct CameraSettings: Codable {
|
||||||
var photoQuality: PhotoQuality
|
var photoQuality: PhotoQuality
|
||||||
var isRingLightEnabled: Bool
|
var isRingLightEnabled: Bool
|
||||||
var ringLightColor: Color
|
var ringLightColorRGB: CustomColorRGB
|
||||||
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,
|
||||||
ringLightColor: .white,
|
ringLightColorRGB: .defaultWhite,
|
||||||
ringLightSize: 25,
|
ringLightSize: 25,
|
||||||
ringLightOpacity: 1.0,
|
ringLightOpacity: 1.0,
|
||||||
flashMode: .off,
|
flashMode: .off,
|
||||||
|
|||||||
@ -8,34 +8,123 @@
|
|||||||
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"
|
||||||
|
]
|
||||||
|
|
||||||
// In UI tests it’s 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.
|
for title in preferredButtons where alert.buttons[title].exists {
|
||||||
|
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 testExample() throws {
|
func testAppStorePortraitScreenshots() throws {
|
||||||
// UI tests must launch the application that they test.
|
app.launchArguments = ["-ui-testing-screenshots"]
|
||||||
let app = XCUIApplication()
|
app.launchEnvironment = [
|
||||||
|
"ENABLE_DEBUG_PREMIUM": "1"
|
||||||
|
]
|
||||||
app.launch()
|
app.launch()
|
||||||
|
if !waitForCameraScreen(timeout: 8) {
|
||||||
|
app.terminate()
|
||||||
|
app.launch()
|
||||||
|
}
|
||||||
|
|
||||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
saveScreenshot(named: "01-launch-screen")
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,13 +21,6 @@ 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
80
docs/APP_REVIEW_CHECKLIST.md
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
# 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.
|
||||||