Skip to content

feat(relay): activate Cloud Relay — server setup, yearly plan, verified subscriptions (v1.5.0)#23

Merged
psimaker merged 5 commits into
mainfrom
feature/cloud-relay-activation
May 31, 2026
Merged

feat(relay): activate Cloud Relay — server setup, yearly plan, verified subscriptions (v1.5.0)#23
psimaker merged 5 commits into
mainfrom
feature/cloud-relay-activation

Conversation

@psimaker

@psimaker psimaker commented May 31, 2026

Copy link
Copy Markdown
Owner

🚀 Commits

  • 393a8bf (iOS)
  • 7ecf918 (docs)
  • 85757df (release v1.5.0)

✨ Highlights

  • Guided Setup for vaultsync-notify: Surfaces the mandatory server-side helper with a guided, copyable setup shown right after purchase.
  • Honest Cloud Relay Status: Shows actual delivery status (waiting-for-server vs delivering) instead of just 'ready' on subscription alone.
  • Pricing & Plans: Adds a yearly plan and raises the monthly price (prices are always read directly from StoreKit).
  • In-Context Upsell: Triggered at the first successful sync, plus a dashboard upgrade affordance.
  • Security & Verification: App now sends the signed StoreKit transaction (JWS) for relay-side verification.

⚙️ Backend Changes (Relay v1.1.0)

💡 Note: The relay code is not part of this repository. These changes are already deployed live on the relay server and documented in the server DEVLOG.

  • JWS verification against Apple's certificate chain.
  • Server-side expiry enforcement.
  • iPhone/iPad multi-device support.

🏷️ Release & Deployment

  • Post-Merge Action: Tag v1.5.0.
  • Constraint: version-check.yml requires tag = ios/project.yml = CHANGELOG = 1.5.0.

🛠️ Manual Tasks (App Store Connect)

  • Created the yearly product: eu.vaultsync.app.relay.yearly
  • Raised the monthly price

psimaker added 3 commits May 31, 2026 14:25
…t status, yearly plan, in-context upsell

The biggest gap was that buyers never learned Cloud Relay needs a one-time
server-side helper (vaultsync-notify), so most subscriptions delivered nothing.
This makes the requirement explicit and improves conversion:

- Setup checklist no longer reports Cloud Relay "ready" on subscription alone;
  it reflects real delivery (waiting for server → finish setup → active once a
  trigger is actually received).
- New RelayServerSetupView: explains the helper and gives a copyable docker
  command (relay URL pre-filled), shown right after purchase and from Settings.
- Settings → Cloud Relay shows a real delivery-status row (delivering / waiting
  for your server / last wake-up) and an honest footer.
- Pricing: raise monthly to a higher reference price and add a yearly plan
  (eu.vaultsync.app.relay.yearly); SubscriptionManager loads both products and
  the UI offers both. No free trial.
- In-context upsell (CloudRelayUpsellView) at the first-successful-sync "aha
  moment" plus a persistent dashboard upgrade affordance for non-subscribers;
  after purchase it flows straight into server setup.
- Provisioning now sends the StoreKit signed transaction (JWS) so the relay can
  verify the subscription with Apple and read its expiry.

All localized in en/de/es/zh. Build + tests green (23 tests, 0 failures).
… Store Connect steps

- relay-spec.md: provisioning now sends the signed transaction (JWS), verified
  offline against Apple's certificate chain; verified expiry gates the
  subscription server-side; legacy numeric IDs still accepted. Multi-device
  (iPhone + iPad) per Device ID works; invalid tokens self-heal via APNs.
  Pricing reference updated (monthly + yearly, no trial).
- PRIVACY.md: transaction data is verified against Apple's certificate chain;
  dropped the unimplemented 90-day purge claim in favour of the actual
  invalid-token cleanup behaviour.
- docs/app-store-connect-relay.md: manual App Store Connect steps for the price
  raise and the new yearly product, plus 3.1.2(a) framing for the setup step.
- ios/project.yml: 1.4.0 → 1.5.0 (build 25 → 26), app + widget targets
- CHANGELOG.md: 1.5.0 section (guided server setup, yearly plan, in-context
  upsell, honest delivery status, verified subscriptions + expiry, iPhone/iPad
  multi-device wake-ups)
- README.md: What's New → v1.5.0, monthly-or-yearly pricing, requirements table
@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@psimaker, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 33 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e2f26fe6-0767-49ae-ba08-a9ce183b06b9

📥 Commits

Reviewing files that changed from the base of the PR and between 85757df and 01dbaca.

📒 Files selected for processing (13)
  • PRIVACY.md
  • docs/relay-spec.md
  • ios/VaultSync.storekit
  • ios/VaultSync/Services/SubscriptionManager.swift
  • ios/VaultSync/ViewModels/SetupChecklistViewModel.swift
  • ios/VaultSync/Views/CloudRelayUpsellView.swift
  • ios/VaultSync/Views/RelayServerSetupView.swift
  • ios/VaultSync/Views/SettingsView.swift
  • ios/VaultSync/de.lproj/Localizable.strings
  • ios/VaultSync/en.lproj/Localizable.strings
  • ios/VaultSync/es.lproj/Localizable.strings
  • ios/VaultSync/zh-Hans.lproj/Localizable.strings
  • ios/VaultSyncTests/SetupChecklistViewModelTests.swift
📝 Walkthrough

Walkthrough

This PR implements Cloud Relay v1.5.0, adding yearly subscription support alongside the existing monthly plan, switching from numeric StoreKit transaction IDs to offline-verifiable JWS for provisioning, and introducing guided UI flows for subscription and server setup.

Changes

Cloud Relay v1.5.0 Implementation

Layer / File(s) Summary
StoreKit Product Configuration and SubscriptionManager Refactor
ios/VaultSync.storekit, ios/VaultSync/Services/SubscriptionManager.swift, ios/VaultSync/Services/RelayService.swift
Monthly price increases from $0.99 to $1.99, and yearly plan ($14.99) is added to StoreKit config. SubscriptionManager introduces monthlyProductID and yearlyProductID constants, stores monthlyProduct and yearlyProduct separately, computes availableProduct from both, adds priceText(for:) helper for localized "price/period" formatting, and makes RelayService.relayURL module-accessible for UI embedding.
JWS-Based Relay Provisioning
ios/VaultSync/Services/SubscriptionManager.swift, docs/relay-spec.md
Purchase API changes to accept explicit Product parameter. Provisioning switches from sending numeric transaction.originalID to verificationResult.jwsRepresentation (signed JWS). Relay verification moves from App Store Server API calls to offline JWS verification against Apple's certificate chain. Supports legacy numeric transaction IDs for backward compatibility.
Cloud Relay Upsell and Server Setup Screens
ios/VaultSync/Views/CloudRelayUpsellView.swift, ios/VaultSync/Views/RelayServerSetupView.swift, ios/VaultSync/Views/ExternalLinkButton.swift
CloudRelayUpsellView conditionally renders subscription pitch (benefits, monthly/yearly buttons, restore-purchases) or post-subscribe confirmation with navigation to server setup. RelayServerSetupView provides three-step server helper instructions, displays prefilled Docker command with pasteboard copy, shows success banner when delivery confirmed, and links to full setup guide.
ContentView and SettingsView Integration
ios/VaultSync/Views/ContentView.swift, ios/VaultSync/Views/SettingsView.swift
ContentView adds one-time upsell sheet triggered when relay unsubscribed + folders exist, includes "Turn on Cloud Relay" dashboard button. SettingsView refactors subscribe UI to separate monthly/yearly buttons, adds server-setup sheet, displays relay delivery status with last-trigger timestamp, shows per-period pricing text, and provides "Set Up Your Server" navigation link when subscribed.
Setup Checklist and Version Bump
ios/VaultSync/ViewModels/SetupChecklistViewModel.swift, ios/project.yml
Relay checklist now requires subscription and at least one received wake-up trigger to mark complete; three distinct states guide users through subscribe → setup → confirm flow. App version bumped 1.4.0/25 → 1.5.0/26.
Documentation, Privacy Policy, Localizations, and Release Notes
CHANGELOG.md, PRIVACY.md, README.md, docs/app-store-connect-relay.md, docs/relay-spec.md, ios/VaultSync/*/Localizable.strings
CHANGELOG documents v1.5.0 features (yearly plan, guided setup, honest delivery status). PRIVACY.md specifies JWS transaction storage and APNs invalid-token cleanup. README clarifies monthly-or-yearly subscription and updates feature summary. App Store Connect guide covers price increase and yearly product creation with review notes. Relay spec updated to JWS provisioning, offline verification, and token cleanup behavior. English, German, Spanish, and Simplified Chinese localizations added for onboarding, subscription, server setup steps, delivery diagnostics, and status messages.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • psimaker/vaultsync#11: Updates relay subscription manager logic for StoreKit-derived pricing and delivery diagnostics, providing foundation context for the monthly/yearly product refactor in this PR.

Poem

🔐 JWS signatures flow from Apple's chain,
📱 Two subscription paths now share the gain,
🔧 Step-by-step setup with Docker so plain,
✨ Yearly and monthly—instant refrain!

🚥 Pre-merge checks | ✅ 7
✅ Passed checks (7 passed)
Check name Status Explanation
Title check ✅ Passed Title uses conventional-commit style (feat:), clearly describes the main change (Cloud Relay activation with server setup, yearly plan, verified subscriptions), and includes the version bump (v1.5.0) reflecting the comprehensive nature of this release.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
No Private Note Leakage ✅ Passed No logging of note contents, filenames, vault paths, API keys, APNs tokens, or StoreKit receipts detected. JWS and tokens never logged; device IDs truncated; no analytics SDKs.
Bounded Ios Background Work ✅ Passed PR introduces no unbounded background work. Tasks are user-initiated with timeouts; no background assertions, URLSessionConfiguration.background, or timers added.
Bridge Contract Compatibility ✅ Passed PR contains no changes to Go bridge files, gomobile-exported functions, JSON response shapes, or bridge test contracts. Swift-Go bridge compatibility maintained.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/cloud-relay-activation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ios/VaultSync/Services/SubscriptionManager.swift (1)

321-347: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Handle yearly entitlements in the transaction pipeline.

handle(_:) still checks transaction.productID == Self.relayProductID, which is the monthly SKU only. A verified yearly purchase or renewal now falls through, gets finish()ed, and never marks the user subscribed or provisions the relay for the purchased devices.

Suggested fix
-        if transaction.productID == Self.relayProductID {
+        if Self.relayProductIDs.contains(transaction.productID) {
             if let revocationDate = transaction.revocationDate {
                 logger.info("Subscription revoked on \(revocationDate)")
                 isRelaySubscribed = false
                 subscriptionExpiryDate = nil
                 await deprovisionRelay()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ios/VaultSync/Services/SubscriptionManager.swift` around lines 321 - 347, The
code only matches Self.relayProductID (monthly SKU) in handle(_:) so yearly
purchases fall through; update the product check to accept both monthly and
yearly SKUs (e.g. add a Self.relayYearlyProductID or a helper like
isRelayProduct(productID:)) and use that in the existing branch where it sets
isRelaySubscribed, subscriptionExpiryDate, stores/loads device IDs
(storeDeviceIDs(_:), loadStoredDeviceIDs()), calls
ensureProvisionStateEntries(for:), and invokes
provisionRelay(deviceIDs:transactionID:) with
verificationResult.jwsRepresentation; keep the revocation/expiration handling
and deprovisionRelay() path intact for non-matching/expired cases so yearly
transactions are processed the same as monthly ones.
🧹 Nitpick comments (1)
ios/VaultSync/Views/RelayServerSetupView.swift (1)

19-27: ⚡ Quick win

Pin the helper image to a tested tag.

This setup command pulls vaultsync-notify:latest, so the onboarding flow can drift away from the app/release contract without any app update. Please use a documented version tag or a shared constant for the tested helper release instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ios/VaultSync/Views/RelayServerSetupView.swift` around lines 19 - 27, The
dockerCommand currently hardcodes the helper image with :latest which can drift;
change it to use a pinned, documented tag by replacing
"ghcr.io/psimaker/vaultsync-notify:latest" with a shared constant (e.g.,
HelperImage.version or RelayService.helperImageTag) and reference that constant
in the dockerCommand string interpolation (keep RelayService.relayURL as-is);
ensure the constant is defined near related networking/relay code and documented
so the onboarding flow uses a tested release rather than latest.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/relay-spec.md`:
- Around line 223-224: The pricing bullet is inconsistent with the /provision
API contract: remove the line that says "App provisions the relay after
successful purchase using the StoreKit transaction ID" and replace it with a
statement that provisioning uses the signed StoreKit transaction JWS per the
/provision endpoint (i.e., the app submits the signed JWS to /provision rather
than a raw transaction ID), ensuring the pricing summary references the
JWS-based provisioning flow described elsewhere in the spec.

In `@ios/VaultSync/Views/SettingsView.swift`:
- Around line 318-352: The subscribeButton(for:label:accent:) currently allows
purchases when syncthingManager.devices.isEmpty so
subscriptionManager.purchase(product, homeserverDeviceIDs:) can be called with
an empty array; update the button to block purchasing until at least one
homeserver device exists by checking syncthingManager.devices.isEmpty before
starting the Task (or adding .disabled(...) to the Button) and, when empty, set
alertMessage to a user-visible prompt instructing the user to add a homeserver
device and set showAlert = true instead of calling subscriptionManager.purchase;
reference subscribeButton(for:label:accent:), syncthingManager.devices, and
subscriptionManager.purchase(...) when making this change.

In `@PRIVACY.md`:
- Line 17: The GDPR deletion sentence currently only promises deletion of the
"original transaction ID" but the document's data list (`**StoreKit transaction
information**`) now declares both the original transaction ID and the
subscription expiry; update the GDPR deletion sentence to reference deletion of
the full stored transaction records (e.g., "original transaction ID and
subscription expiry" or "all stored transaction records") so the deletion scope
matches the declared StoreKit data; locate the sentence that mentions deletion
of the transaction ID and broaden its wording to cover both fields and any
related transaction metadata.

---

Outside diff comments:
In `@ios/VaultSync/Services/SubscriptionManager.swift`:
- Around line 321-347: The code only matches Self.relayProductID (monthly SKU)
in handle(_:) so yearly purchases fall through; update the product check to
accept both monthly and yearly SKUs (e.g. add a Self.relayYearlyProductID or a
helper like isRelayProduct(productID:)) and use that in the existing branch
where it sets isRelaySubscribed, subscriptionExpiryDate, stores/loads device IDs
(storeDeviceIDs(_:), loadStoredDeviceIDs()), calls
ensureProvisionStateEntries(for:), and invokes
provisionRelay(deviceIDs:transactionID:) with
verificationResult.jwsRepresentation; keep the revocation/expiration handling
and deprovisionRelay() path intact for non-matching/expired cases so yearly
transactions are processed the same as monthly ones.

---

Nitpick comments:
In `@ios/VaultSync/Views/RelayServerSetupView.swift`:
- Around line 19-27: The dockerCommand currently hardcodes the helper image with
:latest which can drift; change it to use a pinned, documented tag by replacing
"ghcr.io/psimaker/vaultsync-notify:latest" with a shared constant (e.g.,
HelperImage.version or RelayService.helperImageTag) and reference that constant
in the dockerCommand string interpolation (keep RelayService.relayURL as-is);
ensure the constant is defined near related networking/relay code and documented
so the onboarding flow uses a tested release rather than latest.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7e7e7c19-a51f-4fad-8bcf-9ef6ce9d13f9

📥 Commits

Reviewing files that changed from the base of the PR and between 31f6561 and 85757df.

📒 Files selected for processing (19)
  • CHANGELOG.md
  • PRIVACY.md
  • README.md
  • docs/app-store-connect-relay.md
  • docs/relay-spec.md
  • ios/VaultSync.storekit
  • ios/VaultSync/Services/RelayService.swift
  • ios/VaultSync/Services/SubscriptionManager.swift
  • ios/VaultSync/ViewModels/SetupChecklistViewModel.swift
  • ios/VaultSync/Views/CloudRelayUpsellView.swift
  • ios/VaultSync/Views/ContentView.swift
  • ios/VaultSync/Views/ExternalLinkButton.swift
  • ios/VaultSync/Views/RelayServerSetupView.swift
  • ios/VaultSync/Views/SettingsView.swift
  • ios/VaultSync/de.lproj/Localizable.strings
  • ios/VaultSync/en.lproj/Localizable.strings
  • ios/VaultSync/es.lproj/Localizable.strings
  • ios/VaultSync/zh-Hans.lproj/Localizable.strings
  • ios/project.yml
📜 Review details
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build & Test
🧰 Additional context used
📓 Path-based instructions (5)
**/*.swift

📄 CodeRabbit inference engine (Custom checks)

For Swift background execution changes, pass if work is bounded, cancellation-aware, handles expiration callbacks, and records errors without leaking private vault data. Fail only when background work can continue unbounded, miss cleanup, or violate iOS background execution constraints.

Files:

  • ios/VaultSync/Services/RelayService.swift
  • ios/VaultSync/Views/ExternalLinkButton.swift
  • ios/VaultSync/ViewModels/SetupChecklistViewModel.swift
  • ios/VaultSync/Views/RelayServerSetupView.swift
  • ios/VaultSync/Views/CloudRelayUpsellView.swift
  • ios/VaultSync/Views/ContentView.swift
  • ios/VaultSync/Services/SubscriptionManager.swift
  • ios/VaultSync/Views/SettingsView.swift
ios/**/*.swift

📄 CodeRabbit inference engine (README.md)

Follow Swift API Design Guidelines for iOS/Swift code

Use Swift strict concurrency where applicable in iOS/Swift code

Files:

  • ios/VaultSync/Services/RelayService.swift
  • ios/VaultSync/Views/ExternalLinkButton.swift
  • ios/VaultSync/ViewModels/SetupChecklistViewModel.swift
  • ios/VaultSync/Views/RelayServerSetupView.swift
  • ios/VaultSync/Views/CloudRelayUpsellView.swift
  • ios/VaultSync/Views/ContentView.swift
  • ios/VaultSync/Services/SubscriptionManager.swift
  • ios/VaultSync/Views/SettingsView.swift

⚙️ CodeRabbit configuration file

ios/**/*.swift: Focus on Swift 6 strict concurrency, Sendable/MainActor correctness, Task cancellation,
retain cycles, memory pressure, SwiftUI observation state, StoreKit/APNs flows, and iOS background execution limits.
Pay special attention to BGAppRefreshTask and BGContinuedProcessingTask behavior, expiration handling,
bounded work, and cleanup when the app is suspended or terminated.

Files:

  • ios/VaultSync/Services/RelayService.swift
  • ios/VaultSync/Views/ExternalLinkButton.swift
  • ios/VaultSync/ViewModels/SetupChecklistViewModel.swift
  • ios/VaultSync/Views/RelayServerSetupView.swift
  • ios/VaultSync/Views/CloudRelayUpsellView.swift
  • ios/VaultSync/Views/ContentView.swift
  • ios/VaultSync/Services/SubscriptionManager.swift
  • ios/VaultSync/Views/SettingsView.swift
**/*

⚙️ CodeRabbit configuration file

**/*: VaultSync syncs private Obsidian notes through Syncthing. Treat data loss,
privacy leaks, security regressions, and broken sync behavior as high priority.
Do not nitpick formatting unless it affects maintainability, correctness, or public API clarity.
Flag any accidental logging, telemetry, crash reporting, or network transfer of note contents,
vault paths, filenames with private context, API keys, APNs tokens, relay keys, or security-scoped bookmark data.

Files:

  • ios/VaultSync/Services/RelayService.swift
  • ios/VaultSync/Views/ExternalLinkButton.swift
  • ios/VaultSync/ViewModels/SetupChecklistViewModel.swift
  • ios/project.yml
  • CHANGELOG.md
  • README.md
  • PRIVACY.md
  • ios/VaultSync/zh-Hans.lproj/Localizable.strings
  • ios/VaultSync/en.lproj/Localizable.strings
  • docs/app-store-connect-relay.md
  • ios/VaultSync/de.lproj/Localizable.strings
  • ios/VaultSync/Views/RelayServerSetupView.swift
  • ios/VaultSync.storekit
  • docs/relay-spec.md
  • ios/VaultSync/Views/CloudRelayUpsellView.swift
  • ios/VaultSync/es.lproj/Localizable.strings
  • ios/VaultSync/Views/ContentView.swift
  • ios/VaultSync/Services/SubscriptionManager.swift
  • ios/VaultSync/Views/SettingsView.swift
ios/project.yml

⚙️ CodeRabbit configuration file

ios/project.yml: This generates the Xcode project and Info.plist. Review changes for bundle ID,
entitlements, background modes, URL schemes, signing settings, and accidental secret exposure.

Files:

  • ios/project.yml
**/*.md

⚙️ CodeRabbit configuration file

**/*.md: Review public documentation for technical accuracy, privacy/security claims, App Store-facing wording,
setup correctness, and consistency with the free app plus optional Cloud Relay subscription model.

Files:

  • CHANGELOG.md
  • README.md
  • PRIVACY.md
  • docs/app-store-connect-relay.md
  • docs/relay-spec.md
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: psimaker/vaultsync

Timestamp: 2026-05-31T12:51:59.545Z
Learning: Use Conventional Commits for commit messages
Learnt from: CR
Repo: psimaker/vaultsync

Timestamp: 2026-05-31T12:51:59.545Z
Learning: Provide clear PR descriptions in pull requests
Learnt from: CR
Repo: psimaker/vaultsync

Timestamp: 2026-05-31T12:51:59.545Z
Learning: Reference docs/setup.md and docs/architecture.md when starting development
Learnt from: CR
Repo: psimaker/vaultsync

Timestamp: 2026-05-31T12:51:59.545Z
Learning: Use Go 1.26+ for building the Go xcframework
Learnt from: CR
Repo: psimaker/vaultsync

Timestamp: 2026-05-31T12:51:59.545Z
Learning: Require Xcode 26+ and XcodeGen for building the iOS app from source
Learnt from: CR
Repo: psimaker/vaultsync

Timestamp: 2026-05-31T12:51:59.545Z
Learning: Include iOS/iPadOS version, VaultSync version, server Syncthing version, Cloud Relay/vaultsync-notify status, and relevant logs or screenshots when reporting bugs
Learnt from: CR
Repo: psimaker/vaultsync

Timestamp: 2026-05-31T12:52:07.473Z
Learning: The Cloud Relay server-side helper setup must be framed as part of the product usage (the 'Set Up Your Server' step shown immediately after purchase) rather than as a gate to unlock paid content, to comply with App Store Guideline 3.1.2(a).
Learnt from: CR
Repo: psimaker/vaultsync

Timestamp: 2026-05-31T12:52:07.473Z
Learning: No free trial or introductory offer is configured for Cloud Relay subscriptions (product decision 2026-05-31).
Learnt from: CR
Repo: psimaker/vaultsync

Timestamp: 2026-05-31T12:52:07.473Z
Learning: When raising the monthly Cloud Relay subscription price, preserve the current price for existing subscribers to avoid churn and consent friction.
Learnt from: CR
Repo: psimaker/vaultsync

Timestamp: 2026-05-31T12:52:32.656Z
Learning: Identity is based on Syncthing Device IDs with no user accounts or API keys required
Learnt from: CR
Repo: psimaker/vaultsync

Timestamp: 2026-05-31T12:52:32.656Z
Learning: No file content is ever transmitted from homeserver to relay; only the Syncthing Device ID is sent
Learnt from: CR
Repo: psimaker/vaultsync

Timestamp: 2026-05-31T12:52:32.656Z
Learning: The central relay has no knowledge of what changed or which files/folders are affected
Learnt from: CR
Repo: psimaker/vaultsync

Timestamp: 2026-05-31T12:52:32.656Z
Learning: APNs payload must be a silent push with no visible content to the user
Learnt from: CR
Repo: psimaker/vaultsync

Timestamp: 2026-05-31T12:52:32.656Z
Learning: The homeserver container is stateless except for configuration
Learnt from: CR
Repo: psimaker/vaultsync

Timestamp: 2026-05-31T12:52:32.656Z
Learning: The central relay is horizontally scalable with stateless request handling and shared database
Learnt from: CR
Repo: psimaker/vaultsync

Timestamp: 2026-05-31T12:52:32.656Z
Learning: Self-hosted relay variant requires users to manage their own Apple Developer Account and APNs credentials
Learnt from: CR
Repo: psimaker/vaultsync

Timestamp: 2026-05-31T12:52:32.656Z
Learning: Cloud Relay subscription is managed via App Store StoreKit 2 with auto-renewable monthly or yearly options and no free trial
🪛 LanguageTool
docs/app-store-connect-relay.md

[style] ~12-~12: Consider using a more formal adjective.
Context: ...ibers pay the new price. ## 2. Add the yearly plan 1. Same subscription group **Clou...

(YEARLY_ANNUAL)

🔇 Additional comments (12)
CHANGELOG.md (1)

7-24: LGTM!

README.md (1)

43-43: LGTM!

Also applies to: 63-66, 182-182

docs/app-store-connect-relay.md (1)

1-28: LGTM!

ios/VaultSync/en.lproj/Localizable.strings (1)

356-356: LGTM!

Also applies to: 534-574

ios/VaultSync/de.lproj/Localizable.strings (1)

356-356: LGTM!

Also applies to: 534-574

ios/VaultSync/es.lproj/Localizable.strings (1)

356-356: LGTM!

Also applies to: 534-574

ios/VaultSync/zh-Hans.lproj/Localizable.strings (1)

356-356: LGTM!

Also applies to: 534-574

ios/project.yml (1)

62-63: LGTM!

Also applies to: 115-116

ios/VaultSync/ViewModels/SetupChecklistViewModel.swift (1)

189-223: ⚡ Quick win

Verify lastRelayTriggerReceivedAt is observable and set correctly.

The three-state relay logic is well-designed and matches the PR objectives. However, the implementation depends on subscriptionManager.lastRelayTriggerReceivedAt (line 204), which is not visible in the provided context. Ensure that:

  • The property exists on SubscriptionManager and is marked @Published or part of an @Observable macro so SwiftUI observes changes
  • The property is set when relay triggers are received (likely in APNs handling code)
  • UI updates propagate when the property transitions from nil to a timestamp

Run the following script to verify the property is defined and observable:

#!/bin/bash
# Description: Verify lastRelayTriggerReceivedAt exists on SubscriptionManager and is observable

# Check if the property is defined in SubscriptionManager
rg -n --type=swift -A2 -B2 'lastRelayTriggerReceivedAt' ios/VaultSync/Services/SubscriptionManager.swift

# Verify SubscriptionManager uses `@Observable` or ObservableObject
rg -n --type=swift '^(`@Observable`|class.*ObservableObject)' ios/VaultSync/Services/SubscriptionManager.swift
ios/VaultSync/Views/CloudRelayUpsellView.swift (1)

1-179: LGTM!

ios/VaultSync/Views/ExternalLinkButton.swift (1)

9-9: LGTM!

ios/VaultSync/Views/ContentView.swift (1)

17-19: LGTM!

Also applies to: 82-99, 102-114, 234-257

Comment thread docs/relay-spec.md Outdated
Comment thread ios/VaultSync/Views/SettingsView.swift
Comment thread PRIVACY.md
psimaker added 2 commits May 31, 2026 15:08
…e purchase, align docs

Addresses CodeRabbit review on the PR:

- SubscriptionManager.handle(_:) matched only the monthly SKU, so a verified
  yearly purchase or renewal fell through without marking the user subscribed or
  provisioning the relay. It now matches both SKUs via relayProductIDs.
- Settings subscribe button blocks the purchase until at least one Syncthing peer
  exists and prompts the user to add their server first — otherwise the relay
  would be provisioned with no Device ID to wake. (Localized en/de/es/zh.)
- docs/relay-spec.md pricing bullet and PRIVACY.md deletion scope aligned with the
  JWS-based provisioning and the stored transaction records (id + expiry).

Build + tests green (23 tests, 0 failures).
…ecklist freshness + tests

- StoreKit ranking: make yearly the higher tier (groupNumber 1) and monthly the
  lower (2) so monthly → yearly is an immediate upgrade, matching the App Store
  Connect setup doc.
- Setup checklist: extract the three-state decision into a pure, unit-tested
  function (relayChecklistState) and base "active" on relayDeliveryConfirmed
  (48h freshness), so the checklist agrees with Settings and Relay Diagnostics
  instead of staying green after the helper stops. Adds a test for all 3 states.
- Localization consistency:
  - keep "Vault" untranslated on the upsell screen (es/zh), matching the rest of
    the app
  - keep the Syncthing web-UI path (Actions → Settings → GUI → API Key) in English
    in Step 1 across de/es/zh, since Syncthing's UI language is independent of the
    app locale
  - reword Step 3 so it no longer quotes a specific Diagnostics label, removing the
    EN-vs-translation phrase mismatch
- Hygiene: remove dead localization keys (old checklist copy) in all four languages
  and the unused relayPriceText property.
- docs/relay-spec.md: mark JWS verification + server-side expiry as v1.5.0.

Build + tests green (XCTest 23/0, Swift Testing 47/0).
@psimaker psimaker merged commit 04c7aed into main May 31, 2026
10 checks passed
@psimaker psimaker deleted the feature/cloud-relay-activation branch May 31, 2026 21:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant