Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ If applicable, add screenshots to help explain the problem.

**Environment:**
- Device: [e.g., iPhone 15 Pro]
- iOS version: [e.g., 26.5]
- iOS version: [e.g., 26.0]
- SunHat version: [e.g., 1.0]

**Console logs**
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,5 @@ CLAUDE.md
AppStore/

## Internal audit/process docs (not needed in public repo)
docs/
docs/open-source-readiness-audit.md
docs/open-source-release-report.md
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ These aren't style preferences. Breaking them is a correctness bug and the chang

## Branching

`main` is always deployable. Every change, including your own, goes through a short-lived `feature/<short-description>` branch (e.g. `feature/dry-period-fix`) merged back via PR — no direct commits to `main`. Bug fixes use `fix/<short-description>` instead of `feature/`. Delete the branch after merge.
`main` is always deployable. Every change, including your own, goes through a short-lived `feature/<short-description>` branch (e.g. `feature/dry-period-fix`) merged back via PR, no direct commits to `main`. Bug fixes use `fix/<short-description>` instead of `feature/`. Delete the branch after merge.

This project doesn't use `develop`/`release`/`hotfix` branches — there's no release train to coordinate, so that overhead isn't worth it here. Revisit if the project grows a real release cadence or more contributors.
This project doesn't use `develop`/`release`/`hotfix` branches, there's no release train to coordinate, so that overhead isn't worth it here. Revisit if the project grows a real release cadence or more contributors.

## Before you open a PR

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Captured on iOS 26 (iPhone). SunHat is iPhone-first. iPad, widget, and watch sur
|---|---|
| Xcode | 26+ |
| Swift | 6.2 |
| Minimum iOS | 26.5 |
| Minimum iOS | 26.0 |
| Dependencies | Google Mobile Ads + User Messaging Platform (SPM), for the ad-supported free tier. Everything else is Apple frameworks. |

No CocoaPods, no Carthage. The only SPM packages are Google Mobile Ads and its
Expand Down
20 changes: 10 additions & 10 deletions SunHat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXShellScriptBuildPhase section */
62688EDB10354F069788083D /* Set Build Number from Git */ = {
62688EDB10354F069788083D /* Set Build Number */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
Expand All @@ -100,14 +100,14 @@
);
inputPaths = (
);
name = "Set Build Number from Git";
name = "Set Build Number";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# Build-number policy:\n# - Xcode Cloud (CI_BUILD_NUMBER set): CFBundleVersion = CI_BUILD_NUMBER.\n# Xcode Cloud numbers are per-workflow and strictly increasing, and this\n# workflow is the only one that archives, so App Store Connect uploads\n# never collide. The CI checkout is never modified: buildnumber.txt is a\n# local-only file (gitignored).\n# - Local Release archives: read buildnumber.txt, increment by 1, write the\n# new number back to the file AND into the built product's Info.plist.\n# Local numbers are throwaway; Xcode Cloud is authoritative for uploads.\nif [ \"$CONFIGURATION\" != \"Release\" ]; then\n exit 0\nfi\n\nPLIST=\"$TARGET_BUILD_DIR/$INFOPLIST_PATH\"\n\nif [ -n \"$CI_BUILD_NUMBER\" ]; then\n echo \"[SunHat] Xcode Cloud build: CFBundleVersion = $CI_BUILD_NUMBER\"\n /usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $CI_BUILD_NUMBER\" \"$PLIST\"\n exit 0\nfi\n\nBUILD_NUMBER_FILE=\"$SRCROOT/buildnumber.txt\"\nif [ ! -f \"$BUILD_NUMBER_FILE\" ]; then\n echo \"1\" > \"$BUILD_NUMBER_FILE\"\nfi\n\nCURRENT=$(cat \"$BUILD_NUMBER_FILE\")\nBUILD_NUMBER=$((CURRENT + 1))\necho \"$BUILD_NUMBER\" > \"$BUILD_NUMBER_FILE\"\n\necho \"[SunHat] Build number: $CURRENT -> $BUILD_NUMBER\"\n/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $BUILD_NUMBER\" \"$PLIST\"\n";
shellScript = "# Release archives read buildnumber.txt, increment it, and write the new\n# number into the built product's Info.plist. buildnumber.txt is gitignored\n# and local to this machine. Every App Store Connect upload needs a unique\n# build number for a given MARKETING_VERSION, so if an upload is rejected as\n# a duplicate, bump the file and archive again.\nif [ \"$CONFIGURATION\" != \"Release\" ]; then\n exit 0\nfi\n\nPLIST=\"$TARGET_BUILD_DIR/$INFOPLIST_PATH\"\nBUILD_NUMBER_FILE=\"$SRCROOT/buildnumber.txt\"\n\nif [ ! -f \"$BUILD_NUMBER_FILE\" ]; then\n echo \"1\" > \"$BUILD_NUMBER_FILE\"\nfi\n\nCURRENT=$(cat \"$BUILD_NUMBER_FILE\")\nBUILD_NUMBER=$((CURRENT + 1))\necho \"$BUILD_NUMBER\" > \"$BUILD_NUMBER_FILE\"\n\necho \"[SunHat] Build number: $CURRENT -> $BUILD_NUMBER\"\n/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $BUILD_NUMBER\" \"$PLIST\"\n";
};
/* End PBXShellScriptBuildPhase section */

Expand Down Expand Up @@ -142,7 +142,7 @@
64120D182E2D64CB0034CB20 /* Sources */,
64120D192E2D64CB0034CB20 /* Frameworks */,
64120D1A2E2D64CB0034CB20 /* Resources */,
62688EDB10354F069788083D /* Set Build Number from Git */,
62688EDB10354F069788083D /* Set Build Number */,
);
buildRules = (
);
Expand Down Expand Up @@ -334,7 +334,7 @@
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 26.5;
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down Expand Up @@ -371,7 +371,7 @@
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 26.5;
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down Expand Up @@ -441,7 +441,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.5;
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
Expand Down Expand Up @@ -500,7 +500,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.5;
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
Expand All @@ -519,7 +519,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = HD39MR492X;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.5;
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = org.wesley.sunhatTests;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -540,7 +540,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = HD39MR492X;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.5;
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = org.wesley.sunhatTests;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down
8 changes: 4 additions & 4 deletions SunHat/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -813,14 +813,14 @@
}
}
},
"A renewal payment failed. Ads stay off while Apple retries — update your billing details to keep Ad-Free." : {
"A renewal payment failed. Ads stay off while Apple retries. Update your billing details to keep Ad-Free." : {
"comment" : "Settings footer during the billing grace period",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "A renewal payment failed. Ads stay off while Apple retries — update your billing details to keep Ad-Free."
"value" : "A renewal payment failed. Ads stay off while Apple retries. Update your billing details to keep Ad-Free."
}
},
"es" : {
Expand Down Expand Up @@ -993,14 +993,14 @@
}
}
},
"Ads are off. Cancel anytime — you keep Ad-Free until the end of the period you already paid for." : {
"Ads are off. Cancel anytime and you keep Ad-Free until the end of the period you already paid for." : {
"comment" : "Settings footer while the Ad-Free subscription is active",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ads are off. Cancel anytime — you keep Ad-Free until the end of the period you already paid for."
"value" : "Ads are off. Cancel anytime and you keep Ad-Free until the end of the period you already paid for."
}
},
"es" : {
Expand Down
4 changes: 2 additions & 2 deletions SunHat/Utilities/AppSupportLinks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ enum AppSupportLinks {
static let feedbackEmail = "weskcode@duck.com"
static let privacyEmail = "weskcode@duck.com"

static let privacyPolicyURL = URL(string: "https://sunhat.app/privacy")!
static let termsOfServiceURL = URL(string: "https://sunhat.app/terms")!
static let privacyPolicyURL = URL(string: "https://sunhat.apphq.online/privacy")!
static let termsOfServiceURL = URL(string: "https://sunhat.apphq.online/terms")!

static func mailURL(to email: String, subject: String, body: String? = nil) -> URL? {
var components = URLComponents()
Expand Down
4 changes: 2 additions & 2 deletions SunHat/Views/Settings/AdFreeSettingsSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,9 @@ struct AdFreeSettingsSection: View {
private var footerText: String {
switch storeManager.entitlementState {
case .active:
String(localized: "Ads are off. Cancel anytime — you keep Ad-Free until the end of the period you already paid for.", comment: "Settings footer while the Ad-Free subscription is active")
String(localized: "Ads are off. Cancel anytime and you keep Ad-Free until the end of the period you already paid for.", comment: "Settings footer while the Ad-Free subscription is active")
case .gracePeriod:
String(localized: "A renewal payment failed. Ads stay off while Apple retries — update your billing details to keep Ad-Free.", comment: "Settings footer during the billing grace period")
String(localized: "A renewal payment failed. Ads stay off while Apple retries. Update your billing details to keep Ad-Free.", comment: "Settings footer during the billing grace period")
case .billingRetry:
String(localized: "Your Ad-Free subscription is paused because a renewal payment failed. Update your billing details to restore it.", comment: "Settings footer during billing retry after the grace period lapsed")
case .notEntitled, .unknown:
Expand Down
2 changes: 1 addition & 1 deletion SunHat/sunhat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ final class StoreRecoveryState: ObservableObject {
}

nonisolated func reportRecoveryFailure(_ error: Error) {
let message = String(localized: "SunHat couldn't load your data and is running in temporary recovery mode. Creating and editing reminders is disabled until this is fixed. Restart SunHat to try again, or contact support@sunhat.app for help.", comment: "Banner shown when the app is running on a temporary in-memory database")
let message = String(localized: "SunHat couldn't load your data and is running in temporary recovery mode. Creating and editing reminders is disabled until this is fixed. Restart SunHat to try again, or contact weskcode@duck.com for help.", comment: "Banner shown when the app is running on a temporary in-memory database")
logger.error("Persistent store recovery failed: \(error.localizedDescription)")
Task { @MainActor in
recoveryMessage = message
Expand Down
Loading