Compare commits
2 Commits
660974ff8f
...
20dd4db710
| Author | SHA1 | Date | |
|---|---|---|---|
| 20dd4db710 | |||
| 69545b55bc |
@ -32,8 +32,8 @@
|
|||||||
|
|
||||||
/* 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 */
|
||||||
@ -100,8 +100,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>";
|
||||||
@ -164,7 +164,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 +187,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 */
|
||||||
@ -432,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 = 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 +442,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 +463,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 +473,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,13 +492,13 @@
|
|||||||
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;
|
||||||
@ -512,13 +514,13 @@
|
|||||||
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;
|
||||||
@ -533,12 +535,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 +555,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;
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -24,6 +24,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 {
|
||||||
@ -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
|
||||||
|
|||||||
@ -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,10 +1123,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",
|
||||||
@ -1154,14 +1148,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,
|
||||||
@ -1810,6 +1796,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
|
||||||
@ -1960,6 +1954,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
|
||||||
@ -2051,6 +2053,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
|
||||||
@ -2127,6 +2133,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Reset Onboarding" : {
|
||||||
|
"comment" : "A button label that resets the onboarding state and triggers the onboarding flow again on the next app launch.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Restore Purchases" : {
|
"Restore Purchases" : {
|
||||||
"comment" : "A button that restores purchases.",
|
"comment" : "A button that restores purchases.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@ -2200,6 +2210,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" : {
|
||||||
@ -2777,6 +2791,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Show onboarding flow on next app launch" : {
|
||||||
|
"comment" : "A description of what happens when the \"Reset Onboarding\" button is pressed.",
|
||||||
|
"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,
|
||||||
|
|||||||
@ -78,9 +78,7 @@ final class SelfieCamUITests: XCTestCase {
|
|||||||
let onboardingActions = [
|
let onboardingActions = [
|
||||||
"Get Started",
|
"Get Started",
|
||||||
"Continue",
|
"Continue",
|
||||||
"Enable Camera",
|
"Next",
|
||||||
"Enable Microphone",
|
|
||||||
"Enable Photos",
|
|
||||||
"Done"
|
"Done"
|
||||||
]
|
]
|
||||||
let deadline = Date().addingTimeInterval(timeout)
|
let deadline = Date().addingTimeInterval(timeout)
|
||||||
|
|||||||
80
docs/APP_REVIEW_CHECKLIST.md
Normal file
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.
|
||||||
Loading…
Reference in New Issue
Block a user