Selaa lähdekoodia

Prevent paywall config loading from crashing the app.

Replace fatal paywall.json loading with logged fallback defaults and add a build phase that guarantees paywall.json is copied into the app bundle.

Co-authored-by: Cursor <cursoragent@cursor.com>
Hussain Afzal 2 viikkoa sitten
vanhempi
sitoutus
fb91de8b61
2 muutettua tiedostoa jossa 170 lisäystä ja 5 poistoa
  1. 24 0
      smart_printer.xcodeproj/project.pbxproj
  2. 146 5
      smart_printer/PaywallConfig.swift

+ 24 - 0
smart_printer.xcodeproj/project.pbxproj

@@ -59,6 +59,7 @@
 			buildPhases = (
 				272FF2262FD19A2200A87B72 /* Sources */,
 				272FF2272FD19A2200A87B72 /* Frameworks */,
+				F8A1B2C32FF010000043D1E6 /* Ensure paywall.json in bundle */,
 				272FF2282FD19A2200A87B72 /* Resources */,
 			);
 			buildRules = (
@@ -109,6 +110,29 @@
 		};
 /* End PBXProject section */
 
+/* Begin PBXShellScriptBuildPhase section */
+		F8A1B2C32FF010000043D1E6 /* Ensure paywall.json in bundle */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+			);
+			inputPaths = (
+				"$(SRCROOT)/smart_printer/paywall.json",
+			);
+			name = "Ensure paywall.json in bundle";
+			outputFileListPaths = (
+			);
+			outputPaths = (
+				"$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/paywall.json",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "set -euo pipefail\n\nSOURCE_FILE=\"${SRCROOT}/smart_printer/paywall.json\"\nDEST_DIR=\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}\"\nDEST_FILE=\"${DEST_DIR}/paywall.json\"\n\nif [ ! -f \"${SOURCE_FILE}\" ]; then\n  echo \"error: Required paywall config is missing at ${SOURCE_FILE}\"\n  exit 1\nfi\n\nmkdir -p \"${DEST_DIR}\"\ncp \"${SOURCE_FILE}\" \"${DEST_FILE}\"\n";
+		};
+/* End PBXShellScriptBuildPhase section */
+
 /* Begin PBXResourcesBuildPhase section */
 		272FF2282FD19A2200A87B72 /* Resources */ = {
 			isa = PBXResourcesBuildPhase;

+ 146 - 5
smart_printer/PaywallConfig.swift

@@ -1,4 +1,5 @@
 import Foundation
+import OSLog
 import StoreKit
 
 struct PaywallConfig: Codable, Sendable {
@@ -158,6 +159,11 @@ struct PaywallConfig: Codable, Sendable {
 }
 
 extension PaywallConfig {
+    private static let logger = Logger(
+        subsystem: Bundle.main.bundleIdentifier ?? "com.printer-app-all-printers",
+        category: "PaywallConfig"
+    )
+
     /// Applies remote marketing copy only. Product IDs, legal URLs, and disclosures stay bundled.
     func mergingMarketing(from remote: PaywallConfig) -> PaywallConfig {
         PaywallConfig(
@@ -210,12 +216,147 @@ extension PaywallConfig {
     }
 
     static func loadBundled() -> PaywallConfig {
-        guard let url = Bundle.main.url(forResource: "paywall", withExtension: "json"),
-              let data = try? Data(contentsOf: url),
-              let config = try? JSONDecoder().decode(PaywallConfig.self, from: data) else {
-            fatalError("Missing or invalid paywall.json in app bundle.")
+        guard let url = Bundle.main.url(forResource: "paywall", withExtension: "json") else {
+            logger.error("paywall.json is missing from app bundle. Falling back to built-in defaults.")
+            return fallback
+        }
+
+        do {
+            let data = try Data(contentsOf: url)
+            return try JSONDecoder().decode(PaywallConfig.self, from: data)
+        } catch {
+            logger.error("Failed to decode bundled paywall.json: \(error.localizedDescription, privacy: .public). Falling back to built-in defaults.")
+            return fallback
         }
-        return config
+    }
+
+    static var fallback: PaywallConfig {
+        PaywallConfig(
+            leftPanelTitle: "Unlock Premium Features",
+            rightTitle: "Go Premium",
+            rightSubtitle: "Choose a plan to unlock all premium printing tools.",
+            features: [
+                "Unlimited high-quality scans",
+                "Advanced OCR technology",
+                "Draw & print canvas",
+                "Priority support",
+            ],
+            trustItems: [
+                TrustItem(icon: "shield.fill", title: "Secure Payments", subtitle: "Your payment is protected."),
+                TrustItem(icon: "arrow.counterclockwise", title: "Cancel Anytime", subtitle: "Manage subscriptions anytime."),
+                TrustItem(icon: "headphones", title: "24/7 Support", subtitle: "We're here to help."),
+                TrustItem(icon: "lock.fill", title: "Privacy First", subtitle: "Your data stays private."),
+            ],
+            lifetimeTrustItem: TrustItem(
+                icon: "checkmark.seal.fill",
+                title: "One-Time Purchase",
+                subtitle: "Pay once, no recurring charges."
+            ),
+            plans: Plans(
+                monthly: PlanCopy(
+                    productID: "com.printer_app_all_printers.premium.monthly",
+                    title: "Monthly",
+                    subtitleTemplate: "{price} / month, cancel anytime",
+                    ctaTemplate: "Subscribe for {price} / Month",
+                    fallbackPrice: "—",
+                    priceSuffix: "/mo",
+                    fallbackBillingDescription: "Billed at {price} every month",
+                    features: ["Unlimited scans", "Advanced OCR", "Draw & print canvas"]
+                ),
+                yearly: PlanCopy(
+                    productID: "com.printer_app_all_printers.premium.yearly",
+                    title: "Yearly",
+                    subtitleTemplate: "{price} / year, cancel anytime",
+                    trialSubtitleTemplate: "{trial} free, then {price} / year",
+                    ctaTemplate: "Subscribe for {price} / Year",
+                    fallbackPrice: "—",
+                    priceSuffix: "/yr",
+                    fallbackBillingDescription: "Billed at {price} every year",
+                    features: ["Unlimited scans", "Advanced OCR", "Draw & print canvas"]
+                ),
+                lifetime: PlanCopy(
+                    productID: "com.printer_app_all_printers.premium.lifetime",
+                    title: "Lifetime",
+                    subtitleTemplate: "{price} once, lifetime access",
+                    ctaTemplate: "Buy Lifetime Access for {price}",
+                    fallbackPrice: "—",
+                    priceSuffix: nil,
+                    fallbackBillingDescription: "One-time payment of {price}",
+                    features: ["Unlimited scans", "Advanced OCR", "Draw & print canvas"]
+                )
+            ),
+            footer: Footer(
+                continueFree: "Continue with free plan",
+                manageSubscription: "Manage Subscription",
+                restorePurchase: "Restore Purchase",
+                privacyPolicy: "Privacy Policy",
+                termsOfService: "Terms of Service",
+                support: "Support"
+            ),
+            urls: URLs(
+                privacy: "https://sites.google.com/view/smartprinterappmacos/privacy-policy",
+                terms: "https://sites.google.com/view/smartprinterappmacos/terms-and-condition",
+                support: "https://sites.google.com/view/smartprinterappmacos/get-support",
+                manageSubscriptions: "https://apps.apple.com/account/subscriptions"
+            ),
+            trial: Trial(
+                eligiblePlan: "yearly",
+                badgeTemplate: "{duration} Free Trial",
+                ctaTemplate: "Start {duration} Free Trial",
+                fallbackDuration: TrialFallbackDuration(count: 3, unit: "day")
+            ),
+            duration: Duration(
+                units: DurationUnits(
+                    day: DurationUnit(one: "Day", other: "Days", lowerOne: "day", lowerOther: "days"),
+                    week: DurationUnit(one: "Week", other: "Weeks", lowerOne: "week", lowerOther: "weeks"),
+                    month: DurationUnit(one: "Month", other: "Months", lowerOne: "month", lowerOther: "months"),
+                    year: DurationUnit(one: "Year", other: "Years", lowerOne: "year", lowerOther: "years")
+                ),
+                countUnitTemplate: "{count} {unit}",
+                unknownFallback: "{count} days"
+            ),
+            messages: Messages(
+                productNotFound: "The selected plan is not available right now. Please try again later.",
+                failedVerification: "We couldn't verify your purchase. Please contact support.",
+                noPlansAvailable: "No subscription plans are available right now.",
+                plansLoadFailed: "Couldn't load subscription plans. Check your connection and try again.",
+                purchaseFailedTitle: "Purchase Failed",
+                restoreNotFoundTitle: "No Purchases Found",
+                restoreNotFoundMessage: "We couldn't find any previous purchases for this Apple ID.",
+                restoreSuccessTitle: "Purchases Restored",
+                restoreSuccessMessage: "Your premium access has been restored.",
+                pendingPurchaseTitle: "Purchase Pending",
+                pendingPurchaseMessage: "Your purchase is waiting for approval. You'll get access once it's approved.",
+                alertOK: "OK",
+                retry: "Retry",
+                retrying: "Retrying…",
+                periodFallback: "period",
+                productsLoadTimeout: "Couldn't load subscription products right now. Check your connection and try again.",
+                subscriptionUnavailable: "This subscription is currently unavailable. Please try again in a moment.",
+                planUnavailable: "This plan isn't available right now. Please try again later.",
+                noActiveSubscriptions: "No active subscriptions were found for this Apple ID.",
+                restoreSyncTimeout: "Couldn't reach the App Store in time. Check your connection and tap Restore Purchases again.",
+                purchaseGeneric: "Something went wrong with your subscription. Please try again."
+            ),
+            sidebar: Sidebar(
+                unlockTitle: "Unlock Premium",
+                proTitle: "Printer App - All Printers Pro",
+                unlockDescription: "Upgrade to Premium for unlimited scans, printing, OCR, and all features.",
+                proDescription: "You have unlimited scans, printing, OCR and all premium features.",
+                upgradeAction: "Upgrade Now",
+                manageSubscriptionAction: "Manage Subscription",
+                lifetimeAction: "Premium Active"
+            ),
+            loadingPrice: "—",
+            loadingCTA: "Loading plans…",
+            subscriptionDisclosureTemplate: "Payment will be charged to your Apple ID. Subscription automatically renews at {price} per {period} unless canceled at least 24 hours before the end of the current period.",
+            trialDisclosureTemplate: "After your {trial} free trial, payment will be charged to your Apple ID. Subscription automatically renews at {price} per {period} unless canceled at least 24 hours before the end of the current period.",
+            lifetimeDisclosure: "One-time purchase. No subscription or recurring charges.",
+            loadingDisclosure: "Payment will be charged to your Apple ID. Subscription automatically renews unless canceled at least 24 hours before the end of the current period.",
+            securePaymentNote: "Secure payment. Cancel anytime.",
+            lifetimeSecurePaymentNote: "Secure payment. One-time purchase.",
+            closeButtonHelp: "Close"
+        )
     }
 
     func formatDuration(count: Int, unit: Product.SubscriptionPeriod.Unit, lowercase: Bool) -> String {