|
@@ -1,4 +1,5 @@
|
|
|
import Foundation
|
|
import Foundation
|
|
|
|
|
+import OSLog
|
|
|
import StoreKit
|
|
import StoreKit
|
|
|
|
|
|
|
|
struct PaywallConfig: Codable, Sendable {
|
|
struct PaywallConfig: Codable, Sendable {
|
|
@@ -158,6 +159,11 @@ struct PaywallConfig: Codable, Sendable {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
extension PaywallConfig {
|
|
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.
|
|
/// Applies remote marketing copy only. Product IDs, legal URLs, and disclosures stay bundled.
|
|
|
func mergingMarketing(from remote: PaywallConfig) -> PaywallConfig {
|
|
func mergingMarketing(from remote: PaywallConfig) -> PaywallConfig {
|
|
|
PaywallConfig(
|
|
PaywallConfig(
|
|
@@ -210,12 +216,147 @@ extension PaywallConfig {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
static func loadBundled() -> 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 {
|
|
func formatDuration(count: Int, unit: Product.SubscriptionPeriod.Unit, lowercase: Bool) -> String {
|