diff --git a/SelfieCam/Features/Onboarding/Views/OnboardingPermissionView.swift b/SelfieCam/Features/Onboarding/Views/OnboardingPermissionView.swift index 58a56ed..451ceae 100644 --- a/SelfieCam/Features/Onboarding/Views/OnboardingPermissionView.swift +++ b/SelfieCam/Features/Onboarding/Views/OnboardingPermissionView.swift @@ -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 { - switch self { - case .camera: return String(localized: "Enable Camera") - case .microphone: return String(localized: "Enable Microphone") - case .photoLibrary: return String(localized: "Enable Photos") - } + String(localized: "Continue") } var deniedTitle: String { diff --git a/SelfieCam/Features/Paywall/Views/ProPaywallView.swift b/SelfieCam/Features/Paywall/Views/ProPaywallView.swift index 260bad8..0b256ea 100644 --- a/SelfieCam/Features/Paywall/Views/ProPaywallView.swift +++ b/SelfieCam/Features/Paywall/Views/ProPaywallView.swift @@ -24,6 +24,9 @@ struct ProPaywallView: View { /// Currently selected package @State private var selectedPackage: Package? + + /// Whether products are loading from RevenueCat + @State private var isLoadingProducts = false var body: some View { NavigationStack { @@ -49,10 +52,7 @@ struct ProPaywallView: View { } .font(.system(size: bodyFontSize)) .task { - try? await manager.loadProducts() - if selectedPackage == nil { - selectedPackage = preferredPackage(from: manager.availablePackages) - } + await loadProducts() } .onChange(of: manager.availablePackages) { _, newValue in if selectedPackage == nil { @@ -117,9 +117,25 @@ struct ProPaywallView: View { private var packageSelection: some View { VStack(spacing: Design.Spacing.medium) { - if manager.availablePackages.isEmpty { + if isLoadingProducts { ProgressView() .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 { ForEach(manager.availablePackages, id: \.identifier) { package in PackageOptionRow( @@ -136,6 +152,15 @@ struct ProPaywallView: View { private var purchaseCTA: some View { VStack(spacing: Design.Spacing.small) { 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 { errorMessage = String(localized: "Please select a plan.") showError = true @@ -150,7 +175,12 @@ struct ProPaywallView: View { ProgressView() .tint(.white) } - Text(String(localized: "Continue")) + + Text( + isPurchasing + ? String(localized: "Processing...") + : String(localized: "Continue") + ) .font(.headline) .foregroundStyle(.white) } @@ -159,7 +189,8 @@ struct ProPaywallView: View { .background(AppAccent.primary) .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.")) .font(.caption) @@ -238,6 +269,24 @@ struct ProPaywallView: View { // 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? { if let annual = packages.first(where: { $0.packageType == .annual }) { return annual diff --git a/SelfieCamUITests/SelfieCamUITests.swift b/SelfieCamUITests/SelfieCamUITests.swift index c0702dd..2264ecf 100644 --- a/SelfieCamUITests/SelfieCamUITests.swift +++ b/SelfieCamUITests/SelfieCamUITests.swift @@ -78,9 +78,7 @@ final class SelfieCamUITests: XCTestCase { let onboardingActions = [ "Get Started", "Continue", - "Enable Camera", - "Enable Microphone", - "Enable Photos", + "Next", "Done" ] let deadline = Date().addingTimeInterval(timeout) diff --git a/docs/APP_REVIEW_CHECKLIST.md b/docs/APP_REVIEW_CHECKLIST.md new file mode 100644 index 0000000..a6501b3 --- /dev/null +++ b/docs/APP_REVIEW_CHECKLIST.md @@ -0,0 +1,67 @@ +# App Review Rejection Checklist + +Last updated: 2026-02-12 + +## 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 + +## Guideline 2.1 - App Completeness (IAP not submitted) + +- [ ] In App Store Connect, ensure all IAPs are fully configured (metadata, pricing, localization). +- [ ] 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). +- [ ] Run sandbox purchase tests on iPad form factor before resubmission. + - Review device reported by Apple: iPad Air 11-inch (M3), iPadOS 26.2.1 +- [ ] Confirm Paid Apps Agreement is active. + - Location: App Store Connect -> Agreements, Tax, and Banking + +## Guideline 1.5 - Safety (Support URL) + +- [ ] Update Support URL destination so it clearly provides support contact/help content. + - Current URL: `https://topdoglabs.com/support` +- [ ] Ensure support page includes: + - Contact method (email or form) + - App name(s) supported + - Expected response time + - Troubleshooting/help info + - Link to privacy policy +- [ ] Re-verify URL loads reliably in browser without requiring app login. + +## 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` +- [ ] Verify onboarding permission flow still works for camera/microphone/photo library. + - Note: attempted UITest validation is currently blocked by an existing project test-build issue (`Multiple commands produce ... Selfie_Cam.swiftmodule`). + +## Resubmission Checklist + +- [ ] Increment build number and archive a new build. +- [ ] 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.