Skip to content

Conversation

@AnthonyRonning
Copy link
Contributor

@AnthonyRonning AnthonyRonning commented May 5, 2025

Summary by CodeRabbit

  • New Features

    • Introduced dynamic gating for external billing on iOS, disabling paid plan selection and related UI where external billing is not permitted.
    • Added region detection for iOS App Store to determine billing eligibility.
    • Integrated new store plugin for region-based feature control on iOS, including platform-specific implementations and permissions.
    • Added detailed iOS plugin infrastructure with Swift and Rust components for region querying and plugin management.
    • Added Apple Pay integration with UI components, purchase flow, and backend synchronization.
    • Enhanced billing service with Apple Pay support and transaction syncing.
    • Added permission configurations and schemas for store-related commands.
    • Added utilities for region gating and external billing checks.
  • Bug Fixes

    • Improved synchronization in authentication callback handling.
  • Documentation

    • Added documentation for new iOS plugin APIs and permissions.
  • Chores

    • Updated Rust toolchain version and .gitignore patterns for Rust and iOS build artifacts.

@coderabbitai
Copy link

coderabbitai bot commented May 5, 2025

"""

Walkthrough

This change introduces a new cross-platform Tauri plugin named "store" for detecting the App Store region, implemented in Rust and Swift with corresponding build scripts and permission schemas. The frontend integrates this plugin to dynamically gate external billing on iOS based on the detected region, modifying UI elements and subscription logic accordingly. Supporting infrastructure, configuration, and platform-specific code are added or updated.

Changes

Files/Groups Change Summary
.gitignore, plugins/store/.tauri/tauri-api/.gitignore Updated to ignore Rust and iOS build artifacts, Xcode and SwiftPM files, and other platform-specific outputs.
flake.nix Updated Rust toolchain version from 1.78.0 to 1.81.0.
frontend/src-tauri/Cargo.toml, frontend/src-tauri/src/lib.rs Added the new local store plugin dependency and conditional iOS initialization.
frontend/src/components/Marketing.tsx, frontend/src/routes/pricing.tsx Introduced dynamic gating for external billing on iOS using the new region plugin, affecting UI and subscription logic.
frontend/src/utils/region-gate.ts Added new utility functions to get the App Store region and determine if external billing is allowed based on region.
frontend/src/routes/auth.$provider.callback.tsx Updated effect dependencies in OAuth callback logic to include handleSuccessfulAuth.
plugins/store/Cargo.toml, plugins/store/build.rs Added new Rust plugin manifest and build script for the "store" plugin.
plugins/store/ios/Package.swift, plugins/store/ios/Package.resolved, plugins/store/ios/Sources/StorePlugin.swift Added Swift package definition, dependency resolution, and iOS plugin implementation to retrieve App Store region code and manage in-app purchases.
plugins/store/.tauri/tauri-api/Package.swift, and multiple Swift source files under plugins/store/.tauri/tauri-api/Sources/Tauri/ Added Tauri plugin API Swift sources for channel communication, command invocation, plugin management, JSON handling, logging, and UI utilities.
plugins/store/src/lib.rs, plugins/store/src/commands.rs, plugins/store/src/desktop.rs, plugins/store/src/mobile.rs, plugins/store/src/error.rs Implemented Rust-side plugin logic for desktop and mobile platforms, including command registration, region retrieval, error handling, and platform abstraction.
plugins/store/permissions/autogenerated/commands/*.toml, plugins/store/permissions/autogenerated/reference.md, plugins/store/permissions/schemas/schema.json Added permission configuration, documentation, and JSON schema for the new plugin commands.
frontend/src-tauri/capabilities/default.json, frontend/src-tauri/capabilities/mobile-ios.json Added new store-related permissions to capability JSON configurations.
frontend/src/billing/applePayService.ts Added Apple Pay integration service with product caching, purchase, verification, and subscription status retrieval using Tauri plugin calls.
frontend/src/billing/billingApi.ts Added syncAppleTransaction API call to backend for Apple transaction synchronization.
frontend/src/billing/billingService.ts Extended billing service with Apple Pay sync, Apple Pay visibility, and requirement checks.
frontend/src/components/ApplePayButton.tsx Added new React Apple Pay button component with purchase handling and backend sync.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Frontend (React)
    participant Tauri Backend (Rust)
    participant Store Plugin (Rust/Swift)
    participant App Store (iOS)

    User->>Frontend (React): Navigates to Pricing/Marketing
    Frontend (React)->>Tauri Backend: Checks platform (iOS)
    alt On iOS
        Frontend (React)->>Store Plugin: Invoke get_region
        Store Plugin->>App Store: Query storefront region code
        App Store-->>Store Plugin: Return region code (e.g., "US")
        Store Plugin-->>Frontend (React): Return region code
        Frontend (React)->>Frontend (React): allowExternalBilling() resolves
        alt Region allows external billing
            Frontend (React)->>User: Enable paid plan actions
        else
            Frontend (React)->>User: Show "Coming Soon" (disable paid plans)
        end
    else Not iOS
        Frontend (React)->>User: Show normal billing options
    end
Loading

Possibly related PRs

  • OpenSecretCloud/Maple#42: Added Rust toolchain version 1.78.0 and related Rust tooling support in flake.nix, directly related to this PR’s update of the Rust toolchain version.
  • OpenSecretCloud/Maple#4: Modified frontend/src/routes/pricing.tsx to clarify Bitcoin yearly billing text and display, related by file but implementing different features from this PR.

Poem

In the warren of code, a new plugin appears,
Checking regions for billing, allaying our fears.
Rust and Swift, side by side,
Gating iOS plans with regional pride.
Now bunnies can hop with billing in tune,
"Coming Soon" or "Go!", you’ll find out soon!
🐇💻🍏
"""

Tip

⚡️ Faster reviews with caching
  • CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure Review - Disable Cache at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the Data Retention setting under your Organization Settings.

Enjoy the performance boost—your workflow just got faster.

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented May 5, 2025

Deploying maple with  Cloudflare Pages  Cloudflare Pages

Latest commit: ede3f69
Status:🚫  Build failed.

View logs

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

PR Summary

This PR implements region-based external billing restrictions for iOS users, limiting external billing options to US App Store regions only through a new Tauri plugin and associated frontend changes.

  • Rust version mismatch between flake.nix (1.81.0) and Cargo.toml (1.77.2) could cause build inconsistencies between dev and prod environments
  • iOS StoreKit implementation in StoreRegionPlugin.swift lacks proper error handling for iOS versions below 13.0
  • allowExternalBilling() in region-gate.ts could benefit from more granular error types instead of defaulting all errors to false
  • mobile.rs plugin implementation should consider caching the store region to avoid repeated StoreKit calls
  • Marketing component's dynamic imports could cause UI flicker during region checks - consider adding loading states

12 file(s) reviewed, 10 comment(s)
Edit PR Review Bot Settings | Greptile

@AnthonyRonning AnthonyRonning force-pushed the apple-us-only-external branch from ca8f6f4 to a5decc9 Compare May 5, 2025 21:07
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (9)
plugins/store/.tauri/tauri-api/Sources/Tauri/Logger.swift (2)

5-7: Unnecessary UIKit import

You're importing UIKit when it appears this logger only uses os_log. Consider removing the unnecessary import to reduce dependencies.

import os.log
-import UIKit

24-37: String concatenation could be optimized

The current approach to joining variadic parameters is inefficient. Consider using Array.joined() or String.StringInterpolation for better performance.

  static func log(_ items: Any..., category: String, type: OSLogType) {
    if Logger.enabled {
-      var message = ""
-      let last = items.count - 1
-      for (index, item) in items.enumerated() {
-        message += "\(item)"
-        if index != last {
-          message += " "
-        }
-      }
+      let message = items.map { "\($0)" }.joined(separator: " ")
      let log = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "-", category: category)
      os_log("%{public}@", log: log, type: type, String(message.prefix(4068)))
    }
  }
plugins/store/.tauri/tauri-api/Sources/Tauri/Channel.swift (1)

60-63: Consider using failable String initializer for safer Data to String conversion.

The current implementation directly converts JSON data to a String using String(decoding:as:). While this works, SwiftLint suggests using the failable initializer which provides better error handling.

- handler(id, String(decoding: json, as: UTF8.self))
+ if let jsonString = String(data: json, encoding: .utf8) {
+   handler(id, jsonString)
+ } else {
+   handler(id, "\"Failed to serialize payload\"")
+ }
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 62-62: Prefer failable String(data:encoding:) initializer when converting Data to String

(optional_data_string_conversion)

plugins/store/.tauri/tauri-api/Sources/Tauri/Plugin/Plugin.swift (1)

27-31: Avoid force unwrapping in parseConfig method.

The current implementation force unwraps the result of data(using: .utf8)! which could crash if the config string can't be encoded as UTF-8. Consider handling this case more gracefully.

- let jsonData = self.config.data(using: .utf8)!
- let decoder = JSONDecoder()
- return try decoder.decode(type, from: jsonData)
+ guard let jsonData = self.config.data(using: .utf8) else {
+   throw NSError(domain: "TauriPlugin", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to encode config as UTF-8"])
+ }
+ let decoder = JSONDecoder()
+ return try decoder.decode(type, from: jsonData)
plugins/store/permissions/schemas/schema.json (1)

1-33: Add additionalProperties: false at the top level to avoid silent schema drift

Without this guard, any miss-typed or undocumented field added to a permission file will be silently accepted by consumers even though your schema will not describe it. Locking the schema down early prevents future breaking changes.

   "type": "object",
+  "additionalProperties": false,
plugins/store/.tauri/tauri-api/Sources/Tauri/Tauri.swift (1)

41-47: Prefer where clause instead of if inside a for

Swift-lint already warned about this. The intent is clearer and you skip an unnecessary branch.

-    for (_, handle) in plugins {
-      if !handle.loaded {
-        handle.instance.load(webview: webview)
-      }
-    }
+    for (_, handle) in plugins where !handle.loaded {
+      handle.instance.load(webview: webview)
+    }
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 43-43: where clauses are preferred over a single if inside a for

(for_where)

plugins/store/.tauri/tauri-api/Sources/Tauri/JSTypes.swift (3)

24-61: Consider consistent type conversion handling across getter methods.

The getFloat method handles conversion from Double to Float, but other numeric getter methods don't have similar cross-type handling. For consistency, consider adding similar conversions for other numeric types where appropriate.

public func getInt(_ key: String) -> Int? {
-  return self[key] as? Int
+  if let intValue = self[key] as? Int {
+    return intValue
+  } else if let doubleValue = self[key] as? Double {
+    return Int(doubleValue)
+  }
+  return nil
}

83-83: Consider configuring the date formatter explicitly.

The dateStringFormatter is initialized without any specific configuration. While ISO8601DateFormatter uses a standard format by default, consider explicitly setting options for clarity and to ensure consistent behavior across different iOS versions.

-private let dateStringFormatter = ISO8601DateFormatter()
+private let dateStringFormatter: ISO8601DateFormatter = {
+  let formatter = ISO8601DateFormatter()
+  formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
+  return formatter
+}()

85-123: Comprehensive type coercion with recursive handling.

The coercion function effectively handles various Swift and Foundation types, with recursive processing for collections. The cyclomatic complexity suppression is appropriate given the nature of the function.

Consider adding more specific error handling or logging for unrecognized types instead of silently returning nil. This could help with debugging issues in data conversion.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ca8f6f4 and a5decc9.

⛔ Files ignored due to path filters (2)
  • frontend/src-tauri/Cargo.lock is excluded by !**/*.lock
  • plugins/store/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (32)
  • .gitignore (1 hunks)
  • flake.nix (1 hunks)
  • frontend/src-tauri/Cargo.toml (1 hunks)
  • frontend/src-tauri/src/lib.rs (2 hunks)
  • frontend/src/components/Marketing.tsx (8 hunks)
  • frontend/src/routes/auth.$provider.callback.tsx (1 hunks)
  • frontend/src/routes/pricing.tsx (6 hunks)
  • frontend/src/utils/region-gate.ts (1 hunks)
  • plugins/store/.tauri/tauri-api/.gitignore (1 hunks)
  • plugins/store/.tauri/tauri-api/Package.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/README.md (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Channel.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Invoke.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/JSTypes.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/JsonValue.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Logger.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Plugin/Plugin.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Tauri.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/UiUtils.swift (1 hunks)
  • plugins/store/Cargo.toml (1 hunks)
  • plugins/store/build.rs (1 hunks)
  • plugins/store/ios/Package.resolved (1 hunks)
  • plugins/store/ios/Package.swift (1 hunks)
  • plugins/store/ios/Sources/StorePlugin.swift (1 hunks)
  • plugins/store/permissions/autogenerated/commands/get_region.toml (1 hunks)
  • plugins/store/permissions/autogenerated/reference.md (1 hunks)
  • plugins/store/permissions/schemas/schema.json (1 hunks)
  • plugins/store/src/commands.rs (1 hunks)
  • plugins/store/src/desktop.rs (1 hunks)
  • plugins/store/src/error.rs (1 hunks)
  • plugins/store/src/lib.rs (1 hunks)
  • plugins/store/src/mobile.rs (1 hunks)
✅ Files skipped from review due to trivial changes (13)
  • plugins/store/.tauri/tauri-api/README.md
  • plugins/store/permissions/autogenerated/commands/get_region.toml
  • plugins/store/build.rs
  • flake.nix
  • plugins/store/.tauri/tauri-api/Sources/Tauri/UiUtils.swift
  • plugins/store/.tauri/tauri-api/.gitignore
  • plugins/store/ios/Package.resolved
  • plugins/store/ios/Package.swift
  • plugins/store/permissions/autogenerated/reference.md
  • plugins/store/.tauri/tauri-api/Package.swift
  • plugins/store/Cargo.toml
  • plugins/store/ios/Sources/StorePlugin.swift
  • plugins/store/src/desktop.rs
🚧 Files skipped from review as they are similar to previous changes (7)
  • frontend/src-tauri/src/lib.rs
  • frontend/src-tauri/Cargo.toml
  • frontend/src/routes/auth.$provider.callback.tsx
  • .gitignore
  • frontend/src/utils/region-gate.ts
  • frontend/src/routes/pricing.tsx
  • frontend/src/components/Marketing.tsx
🧰 Additional context used
🧬 Code Graph Analysis (3)
plugins/store/src/commands.rs (2)
plugins/store/src/desktop.rs (1)
  • get_region (16-18)
plugins/store/src/mobile.rs (1)
  • get_region (32-36)
plugins/store/src/lib.rs (3)
plugins/store/src/desktop.rs (2)
  • init (5-10)
  • get_region (16-18)
plugins/store/src/mobile.rs (2)
  • init (11-26)
  • get_region (32-36)
plugins/store/src/commands.rs (1)
  • get_region (7-11)
plugins/store/src/mobile.rs (3)
plugins/store/src/desktop.rs (2)
  • init (5-10)
  • get_region (16-18)
plugins/store/src/lib.rs (1)
  • init (33-45)
plugins/store/src/commands.rs (1)
  • get_region (7-11)
🪛 SwiftLint (0.57.0)
plugins/store/.tauri/tauri-api/Sources/Tauri/Channel.swift

[Warning] 62-62: Prefer failable String(data:encoding:) initializer when converting Data to String

(optional_data_string_conversion)

plugins/store/.tauri/tauri-api/Sources/Tauri/Tauri.swift

[Warning] 43-43: where clauses are preferred over a single if inside a for

(for_where)


[Error] 101-101: Force casts should be avoided

(force_cast)


[Warning] 64-64: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)


[Warning] 126-126: Returning Void in a function declaration is redundant

(redundant_void_return)


[Warning] 130-130: Returning Void in a function declaration is redundant

(redundant_void_return)


[Warning] 73-73: TODOs should be resolved (app crashes without this leak)

(todo)

plugins/store/.tauri/tauri-api/Sources/Tauri/Invoke.swift

[Error] 37-37: Force casts should be avoided

(force_cast)


[Error] 100-100: Force casts should be avoided

(force_cast)


[Warning] 70-70: Prefer failable String(data:encoding:) initializer when converting Data to String

(optional_data_string_conversion)

⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: build-ios
  • GitHub Check: build-macos (universal-apple-darwin)
  • GitHub Check: build-linux
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (22)
plugins/store/src/lib.rs (1)

1-45: LGTM! Well-structured Tauri plugin implementation.

The plugin architecture is cleanly designed with appropriate conditional compilation for desktop vs. mobile platforms, proper trait definitions, and a clear initialization flow. The code follows Rust idioms and provides good abstraction through the StoreExt trait.

plugins/store/.tauri/tauri-api/Sources/Tauri/Channel.swift (2)

44-50: LGTM! Good error handling in the serialization method.

The serialize method properly handles potential errors during JSON representation, providing meaningful fallback messages.


1-65: LGTM! Well-implemented channel communication class.

Overall, the Channel class is well-designed with proper error handling for channel definition parsing and data transmission. The multiple send methods provide flexibility for different data types.

🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 62-62: Prefer failable String(data:encoding:) initializer when converting Data to String

(optional_data_string_conversion)

plugins/store/src/mobile.rs (3)

31-37: LGTM! Clean implementation of the region fetching functionality.

The get_region method correctly invokes the mobile plugin's "getRegion" command and properly maps errors.


1-38: LGTM! Well-structured mobile implementation.

The overall design of the mobile implementation is clean, with proper conditional compilation for iOS vs. non-iOS platforms and appropriate error handling.


18-23:

Details

❓ Verification inconclusive

Consider a safer alternative to std::mem::zeroed() for non-iOS platforms.

Using unsafe { std::mem::zeroed() } to create a dummy handle could lead to undefined behavior if the PluginHandle<R> type contains references or other non-trivially-zeroed types. Consider implementing a safer alternative if possible.


🏁 Script executed:

#!/bin/bash
# Check if PluginHandle is a safe type to zero-initialize
rg -A 5 "pub struct PluginHandle" --type rust

Length of output: 47


I couldn’t locate the PluginHandle definition with the previous search. Let’s broaden the search to find any occurrences in Rust source files:


🏁 Script executed:

#!/bin/bash
# Search for any reference or definition of PluginHandle in Rust files
rg -n "PluginHandle" --type rust

Length of output: 279


Let’s locate the PluginHandle definition in the plugin crate to confirm it’s safe to zero-initialize:


🏁 Script executed:

#!/bin/bash
# Search for PluginHandle definition in the plugin crate
rg -nE "pub (struct|enum|type) PluginHandle" -g 'plugins/plugin/src/**/*.rs'

Length of output: 475


Ensure safe zero-initialization of PluginHandle

  • Location: plugins/store/src/mobile.rs (lines 18–23)
    #[cfg(not(target_os = "ios"))]
    let handle = {
        // Dummy handle for non-iOS platforms
        let handle: PluginHandle<R> = unsafe { std::mem::zeroed() };
        handle
    };

Consider avoiding unsafe { std::mem::zeroed() } for PluginHandle<R>, as zero-initializing a type with non-trivial or reference fields can cause undefined behavior. I wasn’t able to locate the PluginHandle definition—please manually verify that it contains only zero-compatible primitives (or implement a safe constructor/Default for a dummy handle) before relying on zeroed().

plugins/store/.tauri/tauri-api/Sources/Tauri/Plugin/Plugin.swift (3)

43-57: LGTM! Clean implementation of event triggering methods.

Both event triggering methods are well-implemented, with proper handling of the generic encodable type and JavaScript object variants.


59-80: LGTM! Well-implemented listener management.

The registration and removal of listeners is handled correctly, with proper parsing of arguments and management of the listeners dictionary.


1-81: LGTM! Well-designed plugin base class.

Overall, the Plugin class is well-designed with appropriate methods for configuration, permissions, and event handling. The approach to event listener management is clean and efficient.

plugins/store/.tauri/tauri-api/Sources/Tauri/Tauri.swift (1)

49-57: loaded flag never updated when webview is nil – potential double-load

If load is called before the webview exists, loaded remains false; once the webview is later supplied, onWebviewCreated will load it again, executing load twice.
Either set loaded = true after any successful setConfig, or move the flag inside Plugin.load.

plugins/store/.tauri/tauri-api/Sources/Tauri/Invoke.swift (1)

92-100: Dictionary merge in reject allocates and force-casts – replace with safe merging

payload as! JsonObject will crash if any value fails to coerce to JsonValue. Build a JsonObject directly instead.

🧰 Tools
🪛 SwiftLint (0.57.0)

[Error] 100-100: Force casts should be avoided

(force_cast)

plugins/store/.tauri/tauri-api/Sources/Tauri/JSTypes.swift (4)

7-19: Good use of protocol-based design for JavaScript value types.

The empty JSValue protocol serves as an effective marker interface for type-safety. The extensions for Swift/Foundation types provide a clean way to represent JavaScript-compatible values in Swift, making the code more maintainable and type-safe.


20-22: Well-defined type aliases for JavaScript data structures.

The JSObject and JSArray type aliases provide clear, semantically meaningful representations of JavaScript objects and arrays, improving code readability and maintainability.


63-81: Good explanation for explicit coercion need.

The comment explaining why explicit coercion is necessary for Foundation classes is helpful. The static methods provide a clean API for type conversion while hiding the complexity of the implementation.


1-4:

Details

✅ Verification successful

Clarify licensing intention with dual SPDX identifiers.

The file contains both Apache-2.0 and MIT license identifiers. While dual licensing is valid, it's unusual to see both SPDX identifiers listed without clarification. Consider adding a comment to explain the dual licensing intent or choose a single license if that was not intentional.


🏁 Script executed:

#!/bin/bash
# Check if other Tauri files use the same dual licensing pattern
find . -path "*/tauri-api/Sources/*" -name "*.swift" -exec grep -l "SPDX-License-Identifier" {} \; | xargs cat | grep -A 1 "SPDX-License-Identifier"

Length of output: 746


Dual licensing header is intentional and consistent
All Swift sources under plugins/store/.tauri/tauri-api/Sources use both Apache-2.0 and MIT SPDX identifiers in their headers. This dual‐license pattern is deliberate and uniform across the Tauri API codebase, so no additional clarification or changes are needed.

plugins/store/src/commands.rs (1)

1-11: LGTM - Command implementation is concise and follows plugin patterns

This implementation correctly exposes the get_region functionality as a Tauri command that can be called from the frontend. The command properly bridges to platform-specific implementations, returning "UNKNOWN" on desktop and the actual region on iOS.

plugins/store/src/error.rs (2)

3-14: Error structure looks good with platform-specific variants

The error handling is well-structured with appropriate variants for different error types. Good use of the #[cfg(feature = "mobile")] attribute to conditionally include the mobile-specific PluginInvoke error variant.


16-23: Implement serde::Serialize correctly for error serialization

The implementation correctly converts errors to strings for serialization, which is essential for propagating errors across the Rust/JavaScript boundary in Tauri.

plugins/store/.tauri/tauri-api/Sources/Tauri/JsonValue.swift (4)

1-4: License header includes both Apache and MIT licenses

The file includes both Apache-2.0 and MIT license headers, which is common for Tauri projects that use dual licensing.


7-14: Type definitions are clean and appropriately scoped

The JsonObject typealias and JsonValue enum with its nested SerializationError are well-defined with appropriate visibility modifiers.


16-29: JSON serialization logic handles additional fields correctly

The implementation appropriately merges additional fields while preserving existing values, prepares the dictionary, validates it, and then serializes it to a JSON string.


31-58: Recursive processing of nested structures is thorough

The recursive processing of dictionaries and arrays handles all key data types, particularly paying attention to properly formatting Date objects using ISO8601.

However, be cautious with extremely deeply nested JSON structures as recursive calls may lead to stack overflow in extreme cases, though this is unlikely in normal usage.

@AnthonyRonning AnthonyRonning force-pushed the apple-us-only-external branch from a5decc9 to ae440c0 Compare May 5, 2025 21:39
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (8)
frontend/src/routes/pricing.tsx (2)

175-175: Remove console.log before production.

This debugging statement should be removed before deploying to production.

-              setExternalBillingAllowed(allowed);
-              console.log("External billing allowed:", allowed);
+              setExternalBillingAllowed(allowed);

452-455: Consider improving user feedback for unavailable external billing.

When external billing is not allowed, the function simply returns without any user feedback. Consider showing a message explaining why this action is not available.

      if (isIOS && !product.name.toLowerCase().includes("free") && !externalBillingAllowed) {
        // Don't allow any action on paid plans if on iOS and external billing not allowed
+       // Show toast or dialog explaining why the action is not available
+       // e.g., "This plan is not available in your App Store region at this time."
        return;
      }
plugins/store/.tauri/tauri-api/Sources/Tauri/Invoke.swift (2)

34-38: ⚠️ Potential issue

Avoid force unwrapping and force casting in getArgs().

The current implementation uses force unwrapping and force casting which could cause crashes if the data isn't properly formatted.

Apply this safer implementation:

  public func getArgs() throws -> JSObject {
-    let jsonData = self.data.data(using: .utf8)!
-    let data = try JSONSerialization.jsonObject(with: jsonData, options: [])
-    return JSTypes.coerceDictionaryToJSObject(
-      (data as! NSDictionary), formattingDatesAsStrings: true)!
+    guard let jsonData = self.data.data(using: .utf8) else {
+      throw NSError(domain: "Invoke", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid UTF-8 data"])
+    }
+    let data = try JSONSerialization.jsonObject(with: jsonData, options: [])
+    guard 
+      let dict = data as? NSDictionary,
+      let result = JSTypes.coerceDictionaryToJSObject(dict, formattingDatesAsStrings: true) 
+    else {
+      throw NSError(domain: "Invoke", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid argument format"])
+    }
+    return result
  }
🧰 Tools
🪛 SwiftLint (0.57.0)

[Error] 37-37: Force casts should be avoided

(force_cast)


67-74: 🛠️ Refactor suggestion

Use the failable String(data:encoding:) initializer in resolve<T>().

The current implementation uses String(decoding:as:) which can lead to data corruption if the encoded JSON is not valid UTF-8.

Apply this safer implementation:

  public func resolve<T: Encodable>(_ data: T) {
    do {
      let json = try JSONEncoder().encode(data)
-      sendResponse(callback, String(decoding: json, as: UTF8.self))
+      guard let jsonString = String(data: json, encoding: .utf8) else {
+        throw NSError(domain: "Invoke", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid UTF-8"])
+      }
+      sendResponse(callback, jsonString)
    } catch {
      sendResponse(self.error, "\"\(error)\"")
    }
  }
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 70-70: Prefer failable String(data:encoding:) initializer when converting Data to String

(optional_data_string_conversion)

plugins/store/.tauri/tauri-api/Sources/Tauri/Tauri.swift (4)

64-64: Remove redundant nil initialization.

Initializing an optional variable with nil is redundant as optionals are nil by default.

-          var error: NSError? = nil
+          var error: NSError?
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 64-64: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)


33-39: ⚠️ Potential issue

Avoid force-unwrapping when composing the asset URL.

Using URL(string: "asset://localhost")! will crash if the string is ever malformed.

Apply this safer implementation:

  public func assetUrl(fromLocalURL url: URL?) -> URL? {
    guard let inputURL = url else {
      return nil
    }

-    return URL(string: "asset://localhost")!.appendingPathComponent(inputURL.path)
+    guard let base = URL(string: "asset://localhost") else {
+      return nil
+    }
+    return base.appendingPathComponent(inputURL.path)
  }

73-75: ⚠️ Potential issue

Address the intentional memory leak.

The comment indicates a deliberate memory leak to prevent app crashes. This should be properly investigated and fixed rather than relying on a leak.

-            // TODO: app crashes without this leak
-            let _ = Unmanaged.passRetained(error)
+            // FIXME: Investigate why the NSError is being over-released and fix the underlying issue
+            // Temporary workaround: We're preventing premature deallocation by retaining the error
+            let _ = Unmanaged.passRetained(error)
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 73-73: TODOs should be resolved (app crashes without this leak)

(todo)


97-104: ⚠️ Potential issue

Replace force-cast with safe down-cast when registering plugins.

Using a force-cast (plugin as! Plugin) will crash if an object that doesn't conform to the Plugin protocol is passed.

Apply this safer implementation:

@_cdecl("register_plugin")
func registerPlugin(name: SRString, plugin: NSObject, config: SRString, webview: WKWebView?) {
-  PluginManager.shared.load(
-    name: name.toString(),
-    plugin: plugin as! Plugin,
-    config: config.toString(),
-    webview: webview
-  )
+  guard let typedPlugin = plugin as? Plugin else {
+    os_log("Attempted to register an object that does not conform to Plugin protocol", type: .fault)
+    return
+  }
+  PluginManager.shared.load(
+    name: name.toString(),
+    plugin: typedPlugin,
+    config: config.toString(),
+    webview: webview
+  )
}
🧰 Tools
🪛 SwiftLint (0.57.0)

[Error] 101-101: Force casts should be avoided

(force_cast)

🧹 Nitpick comments (1)
plugins/store/.tauri/tauri-api/Sources/Tauri/Tauri.swift (1)

41-47: Use for where clause for more readable code.

When iterating and checking a condition, a for where clause is more readable than a separate if statement inside the loop.

  func onWebviewCreated(_ webview: WKWebView) {
-    for (_, handle) in plugins {
-      if !handle.loaded {
-        handle.instance.load(webview: webview)
-      }
-    }
+    for (_, handle) in plugins where !handle.loaded {
+      handle.instance.load(webview: webview)
+    }
  }
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 43-43: where clauses are preferred over a single if inside a for

(for_where)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a5decc9 and ae440c0.

⛔ Files ignored due to path filters (2)
  • frontend/src-tauri/Cargo.lock is excluded by !**/*.lock
  • plugins/store/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (32)
  • .gitignore (1 hunks)
  • flake.nix (1 hunks)
  • frontend/src-tauri/Cargo.toml (1 hunks)
  • frontend/src-tauri/src/lib.rs (2 hunks)
  • frontend/src/components/Marketing.tsx (8 hunks)
  • frontend/src/routes/auth.$provider.callback.tsx (1 hunks)
  • frontend/src/routes/pricing.tsx (8 hunks)
  • frontend/src/utils/region-gate.ts (1 hunks)
  • plugins/store/.tauri/tauri-api/.gitignore (1 hunks)
  • plugins/store/.tauri/tauri-api/Package.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/README.md (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Channel.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Invoke.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/JSTypes.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/JsonValue.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Logger.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Plugin/Plugin.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Tauri.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/UiUtils.swift (1 hunks)
  • plugins/store/Cargo.toml (1 hunks)
  • plugins/store/build.rs (1 hunks)
  • plugins/store/ios/Package.resolved (1 hunks)
  • plugins/store/ios/Package.swift (1 hunks)
  • plugins/store/ios/Sources/StorePlugin.swift (1 hunks)
  • plugins/store/permissions/autogenerated/commands/get_region.toml (1 hunks)
  • plugins/store/permissions/autogenerated/reference.md (1 hunks)
  • plugins/store/permissions/schemas/schema.json (1 hunks)
  • plugins/store/src/commands.rs (1 hunks)
  • plugins/store/src/desktop.rs (1 hunks)
  • plugins/store/src/error.rs (1 hunks)
  • plugins/store/src/lib.rs (1 hunks)
  • plugins/store/src/mobile.rs (1 hunks)
✅ Files skipped from review due to trivial changes (4)
  • plugins/store/permissions/autogenerated/reference.md
  • plugins/store/ios/Package.resolved
  • plugins/store/.tauri/tauri-api/Package.swift
  • plugins/store/.tauri/tauri-api/.gitignore
🚧 Files skipped from review as they are similar to previous changes (25)
  • plugins/store/ios/Package.swift
  • frontend/src/routes/auth.$provider.callback.tsx
  • plugins/store/build.rs
  • frontend/src-tauri/Cargo.toml
  • plugins/store/permissions/autogenerated/commands/get_region.toml
  • flake.nix
  • plugins/store/src/commands.rs
  • .gitignore
  • frontend/src-tauri/src/lib.rs
  • plugins/store/.tauri/tauri-api/README.md
  • plugins/store/Cargo.toml
  • plugins/store/.tauri/tauri-api/Sources/Tauri/UiUtils.swift
  • plugins/store/src/desktop.rs
  • plugins/store/.tauri/tauri-api/Sources/Tauri/JsonValue.swift
  • frontend/src/utils/region-gate.ts
  • plugins/store/src/lib.rs
  • plugins/store/src/error.rs
  • frontend/src/components/Marketing.tsx
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Channel.swift
  • plugins/store/ios/Sources/StorePlugin.swift
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Logger.swift
  • plugins/store/permissions/schemas/schema.json
  • plugins/store/src/mobile.rs
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Plugin/Plugin.swift
  • plugins/store/.tauri/tauri-api/Sources/Tauri/JSTypes.swift
🧰 Additional context used
🧬 Code Graph Analysis (1)
frontend/src/routes/pricing.tsx (1)
frontend/src/utils/region-gate.ts (1)
  • allowExternalBilling (9-17)
🪛 SwiftLint (0.57.0)
plugins/store/.tauri/tauri-api/Sources/Tauri/Tauri.swift

[Warning] 43-43: where clauses are preferred over a single if inside a for

(for_where)


[Error] 101-101: Force casts should be avoided

(force_cast)


[Warning] 64-64: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)


[Warning] 126-126: Returning Void in a function declaration is redundant

(redundant_void_return)


[Warning] 130-130: Returning Void in a function declaration is redundant

(redundant_void_return)


[Warning] 73-73: TODOs should be resolved (app crashes without this leak)

(todo)

plugins/store/.tauri/tauri-api/Sources/Tauri/Invoke.swift

[Error] 37-37: Force casts should be avoided

(force_cast)


[Error] 100-100: Force casts should be avoided

(force_cast)


[Warning] 70-70: Prefer failable String(data:encoding:) initializer when converting Data to String

(optional_data_string_conversion)

⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: build-macos (universal-apple-darwin)
  • GitHub Check: build-linux
  • GitHub Check: build-ios
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (5)
frontend/src/routes/pricing.tsx (5)

151-153: Good implementation of external billing check.

Adding state to track whether external billing is allowed based on region is a good approach for controlling feature availability.


168-180: Good error handling for region check.

The implementation properly handles errors during the region check and defaults to the safer option (disabling external billing) when errors occur.


268-271: Clear UI feedback for region-restricted features.

Using "Coming Soon" as button text for paid plans that aren't available in the user's region provides good user feedback.


682-697: Good conditional rendering of Bitcoin toggle.

The conditional rendering of the Bitcoin toggle ensures it's only shown when appropriate based on platform and region constraints.


813-817: Clear button state handling for region-specific restrictions.

Properly disabling buttons for paid plans on iOS when external billing is not allowed provides a consistent user experience with the "Coming Soon" text.

@AnthonyRonning AnthonyRonning force-pushed the apple-us-only-external branch from ae440c0 to a023139 Compare May 5, 2025 21:52
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (8)
plugins/store/.tauri/tauri-api/Sources/Tauri/Channel.swift (1)

60-63: 🛠️ Refactor suggestion

Avoid lossy String(decoding:as:) – switch to the failable initialiser
Using String(decoding:as:) may silently replace invalid UTF-8 sequences, potentially corrupting the JSON forwarded to JavaScript. Prefer the failable String(data:encoding:) initialiser so you can surface an error when the data is not valid UTF-8.

-    let json = try JSONEncoder().encode(data)
-    handler(id, String(decoding: json, as: UTF8.self))
+    let json = try JSONEncoder().encode(data)
+    guard let jsonString = String(data: json, encoding: .utf8) else {
+      handler(id, "\"Invalid UTF-8 payload\"")
+      return
+    }
+    handler(id, jsonString)
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 62-62: Prefer failable String(data:encoding:) initializer when converting Data to String

(optional_data_string_conversion)

frontend/src/utils/region-gate.ts (1)

3-3: Model region codes with a TS enum or union type
Hard-coding region literals in an array makes accidental typos hard to spot and limits editor auto-completion. Defining an enum/string-literal union (e.g. type USRegion = "USA" | "ASM" | …) improves type-safety and maintainability.

plugins/store/.tauri/tauri-api/Sources/Tauri/Invoke.swift (4)

33-38: ⚠️ Potential issue

Crash-prone force-unwrap & force-cast in getArgs()
data(using:)!, as!, and the trailing ! on the coerced object will crash the app on malformed JSON. Replace with optional binding and throw/reject on failure.

-    let jsonData = self.data.data(using: .utf8)!
-    let data = try JSONSerialization.jsonObject(with: jsonData, options: [])
-    return JSTypes.coerceDictionaryToJSObject(
-      (data as! NSDictionary), formattingDatesAsStrings: true)!
+    guard let jsonData = self.data.data(using: .utf8) else {
+      throw NSError(domain: "Invoke", code: 0,
+                    userInfo: [NSLocalizedDescriptionKey: "Invalid UTF-8 data"])
+    }
+    let obj = try JSONSerialization.jsonObject(with: jsonData, options: [])
+    guard
+      let dict = obj as? NSDictionary,
+      let js = JSTypes.coerceDictionaryToJSObject(dict, formattingDatesAsStrings: true)
+    else {
+      throw NSError(domain: "Invoke", code: 0,
+                    userInfo: [NSLocalizedDescriptionKey: "Arguments are not an object"])
+    }
+    return js
🧰 Tools
🪛 SwiftLint (0.57.0)

[Error] 37-37: Force casts should be avoided

(force_cast)


40-45: ⚠️ Potential issue

Force-unwrap in parseArgs<T>() mirrors the same crash risk
Apply the same guarded pattern as suggested for getArgs() to prevent runtime crashes on invalid UTF-8.


68-72: 🛠️ Refactor suggestion

Use safe UTF-8 conversion when encoding responses
As flagged by SwiftLint, prefer the failable String(data:encoding:) and send a meaningful error if conversion fails.

-      let json = try JSONEncoder().encode(data)
-      sendResponse(callback, String(decoding: json, as: UTF8.self))
+      let json = try JSONEncoder().encode(data)
+      guard let jsonString = String(data: json, encoding: .utf8) else {
+        sendResponse(self.error, "\"Failed to encode response as UTF-8\"")
+        return
+      }
+      sendResponse(callback, jsonString)
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 70-70: Prefer failable String(data:encoding:) initializer when converting Data to String

(optional_data_string_conversion)


100-100: ⚠️ Potential issue

Force-cast payload as! JsonObject can crash on non-dictionary types

Failure to meet this assumption will terminate the app. Safely down-cast and fall back to a descriptive error when the cast fails.

-    sendResponse(self.error, serialize(.dictionary(payload as! JsonObject)))
+    if let jsonPayload = payload as? JsonObject {
+      sendResponse(self.error, serialize(.dictionary(jsonPayload)))
+    } else {
+      sendResponse(self.error, "\"Failed to convert payload to JsonObject\"")
+    }
🧰 Tools
🪛 SwiftLint (0.57.0)

[Error] 100-100: Force casts should be avoided

(force_cast)

plugins/store/.tauri/tauri-api/Sources/Tauri/Tauri.swift (2)

33-39: ⚠️ Potential issue

Fix unsafe force-unwrapped URL creation

The URL constructor is being force-unwrapped, which could crash the app if the hard-coded string is ever changed or malformed.

-  return URL(string: "asset://localhost")!.appendingPathComponent(inputURL.path)
+  guard let baseURL = URL(string: "asset://localhost") else {
+    return nil
+  }
+  return baseURL.appendingPathComponent(inputURL.path)

97-105: ⚠️ Potential issue

Replace force-cast with safe downcasting

The force-cast will crash the app if an object that doesn't conform to the Plugin protocol is registered.

@_cdecl("register_plugin")
-func registerPlugin(name: SRString, plugin: NSObject, config: SRString, webview: WKWebView?) {
-  PluginManager.shared.load(
-    name: name.toString(),
-    plugin: plugin as! Plugin,
-    config: config.toString(),
-    webview: webview
-  )
+func registerPlugin(name: SRString, plugin: NSObject, config: SRString, webview: WKWebView?) {
+  guard let typedPlugin = plugin as? Plugin else {
+    os_log("Attempted to register an object that does not conform to Plugin", type: .fault)
+    return
+  }
+  PluginManager.shared.load(
+    name: name.toString(),
+    plugin: typedPlugin,
+    config: config.toString(),
+    webview: webview
+  )
}
🧰 Tools
🪛 SwiftLint (0.57.0)

[Error] 101-101: Force casts should be avoided

(force_cast)

🧹 Nitpick comments (3)
plugins/store/.tauri/tauri-api/Sources/Tauri/Tauri.swift (2)

41-47: Use for-where clause for cleaner code

Replace the conditional inside the for loop with a more idiomatic Swift pattern.

-  for (_, handle) in plugins {
-    if !handle.loaded {
-      handle.instance.load(webview: webview)
-    }
-  }
+  for (_, handle) in plugins where !handle.loaded {
+    handle.instance.load(webview: webview)
+  }
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 43-43: where clauses are preferred over a single if inside a for

(for_where)


124-133: Remove redundant void return type declarations

The -> Void return type annotations are redundant and can be removed.

  let invoke = Invoke(
    command: command.toString(), callback: callbackId, error: errorId,
-    sendResponse: { (fn: UInt64, payload: String?) -> Void in
+    sendResponse: { (fn: UInt64, payload: String?) in
      let success = fn == callbackId
      callback(id, success, payload ?? "null")
    },
-    sendChannelData: { (id: UInt64, payload: String) -> Void in
+    sendChannelData: { (id: UInt64, payload: String) in
      sendChannelData(id, payload)
    }, data: data.toString())
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 126-126: Returning Void in a function declaration is redundant

(redundant_void_return)


[Warning] 130-130: Returning Void in a function declaration is redundant

(redundant_void_return)

frontend/src/routes/pricing.tsx (1)

178-178: Remove console.log before production

Console logs should be removed before production to avoid leaking implementation details and cluttering the console.

-              console.log("Store region:", region, "External billing allowed:", allowed);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae440c0 and a023139.

⛔ Files ignored due to path filters (2)
  • frontend/src-tauri/Cargo.lock is excluded by !**/*.lock
  • plugins/store/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (32)
  • .gitignore (1 hunks)
  • flake.nix (1 hunks)
  • frontend/src-tauri/Cargo.toml (1 hunks)
  • frontend/src-tauri/src/lib.rs (2 hunks)
  • frontend/src/components/Marketing.tsx (8 hunks)
  • frontend/src/routes/auth.$provider.callback.tsx (1 hunks)
  • frontend/src/routes/pricing.tsx (9 hunks)
  • frontend/src/utils/region-gate.ts (1 hunks)
  • plugins/store/.tauri/tauri-api/.gitignore (1 hunks)
  • plugins/store/.tauri/tauri-api/Package.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/README.md (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Channel.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Invoke.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/JSTypes.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/JsonValue.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Logger.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Plugin/Plugin.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Tauri.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/UiUtils.swift (1 hunks)
  • plugins/store/Cargo.toml (1 hunks)
  • plugins/store/build.rs (1 hunks)
  • plugins/store/ios/Package.resolved (1 hunks)
  • plugins/store/ios/Package.swift (1 hunks)
  • plugins/store/ios/Sources/StorePlugin.swift (1 hunks)
  • plugins/store/permissions/autogenerated/commands/get_region.toml (1 hunks)
  • plugins/store/permissions/autogenerated/reference.md (1 hunks)
  • plugins/store/permissions/schemas/schema.json (1 hunks)
  • plugins/store/src/commands.rs (1 hunks)
  • plugins/store/src/desktop.rs (1 hunks)
  • plugins/store/src/error.rs (1 hunks)
  • plugins/store/src/lib.rs (1 hunks)
  • plugins/store/src/mobile.rs (1 hunks)
✅ Files skipped from review due to trivial changes (5)
  • plugins/store/ios/Package.resolved
  • plugins/store/permissions/autogenerated/reference.md
  • plugins/store/ios/Package.swift
  • plugins/store/.tauri/tauri-api/Package.swift
  • plugins/store/src/desktop.rs
🚧 Files skipped from review as they are similar to previous changes (22)
  • plugins/store/.tauri/tauri-api/README.md
  • plugins/store/permissions/autogenerated/commands/get_region.toml
  • flake.nix
  • .gitignore
  • plugins/store/build.rs
  • frontend/src-tauri/Cargo.toml
  • frontend/src-tauri/src/lib.rs
  • plugins/store/src/commands.rs
  • plugins/store/.tauri/tauri-api/Sources/Tauri/JSTypes.swift
  • frontend/src/routes/auth.$provider.callback.tsx
  • plugins/store/.tauri/tauri-api/Sources/Tauri/UiUtils.swift
  • plugins/store/.tauri/tauri-api/.gitignore
  • plugins/store/Cargo.toml
  • plugins/store/ios/Sources/StorePlugin.swift
  • plugins/store/src/error.rs
  • plugins/store/.tauri/tauri-api/Sources/Tauri/JsonValue.swift
  • plugins/store/src/lib.rs
  • plugins/store/src/mobile.rs
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Plugin/Plugin.swift
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Logger.swift
  • frontend/src/components/Marketing.tsx
  • plugins/store/permissions/schemas/schema.json
🧰 Additional context used
🧬 Code Graph Analysis (3)
frontend/src/utils/region-gate.ts (2)
plugins/store/.tauri/tauri-api/Sources/Tauri/Tauri.swift (1)
  • invoke (59-88)
plugins/store/.tauri/tauri-api/Sources/Tauri/Logger.swift (1)
  • error (55-57)
frontend/src/routes/pricing.tsx (1)
frontend/src/utils/region-gate.ts (2)
  • getStoreRegion (9-18)
  • isUSRegion (34-36)
plugins/store/.tauri/tauri-api/Sources/Tauri/Invoke.swift (3)
plugins/store/.tauri/tauri-api/Sources/Tauri/JSTypes.swift (2)
  • coerceDictionaryToJSObject (70-74)
  • coerceDictionaryToJSObject (76-80)
plugins/store/.tauri/tauri-api/Sources/Tauri/Channel.swift (1)
  • serialize (44-50)
plugins/store/.tauri/tauri-api/Sources/Tauri/JsonValue.swift (1)
  • jsonRepresentation (16-29)
🪛 SwiftLint (0.57.0)
plugins/store/.tauri/tauri-api/Sources/Tauri/Channel.swift

[Warning] 62-62: Prefer failable String(data:encoding:) initializer when converting Data to String

(optional_data_string_conversion)

plugins/store/.tauri/tauri-api/Sources/Tauri/Invoke.swift

[Error] 37-37: Force casts should be avoided

(force_cast)


[Error] 100-100: Force casts should be avoided

(force_cast)


[Warning] 70-70: Prefer failable String(data:encoding:) initializer when converting Data to String

(optional_data_string_conversion)

plugins/store/.tauri/tauri-api/Sources/Tauri/Tauri.swift

[Warning] 43-43: where clauses are preferred over a single if inside a for

(for_where)


[Error] 101-101: Force casts should be avoided

(force_cast)


[Warning] 64-64: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)


[Warning] 126-126: Returning Void in a function declaration is redundant

(redundant_void_return)


[Warning] 130-130: Returning Void in a function declaration is redundant

(redundant_void_return)


[Warning] 73-73: TODOs should be resolved (app crashes without this leak)

(todo)

⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: build-linux
  • GitHub Check: build-macos (universal-apple-darwin)
  • GitHub Check: build-ios
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (5)
plugins/store/.tauri/tauri-api/Sources/Tauri/Tauri.swift (1)

59-88:

Details

❓ Verification inconclusive

Address multiple issues in dynamic invocation block

There are several issues in this method that need attention:

  1. Redundant optional initialization
  2. Unsafe type casting using unsafeBitCast
  3. Deliberate memory leak with a TODO comment
-          var error: NSError? = nil
+          var error: NSError?

The TODO comment indicates a deliberate memory leak. This approach is masking a real memory management issue that should be properly addressed. Can you provide more information about what causes the crash when this leak is removed?

The use of unsafeBitCast for method invocation is brittle. Consider exploring safer alternatives like using @objc protocols with typed wrappers for these interactions where feasible.


Investigate and refactor dynamic invocation in Tauri.swift

  • File: plugins/store/.tauri/tauri-api/Sources/Tauri/Tauri.swift (lines 59–88)
  1. Simplify optional initialization:
    - var error: NSError? = nil
    + var error: NSError?
  2. Replace brittle unsafeBitCast dispatch:
    • Consider using perform(_:with:with:) or defining an @objc protocol so you can call a typed method instead of casting the IMP.
  3. Remove the deliberate leak and address the root cause:
    • What exactly crashes when you drop Unmanaged.passRetained(error)? Please investigate and share the failure mode so we can handle the error pointer safely without leaking memory.
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 64-64: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)


[Warning] 73-73: TODOs should be resolved (app crashes without this leak)

(todo)

frontend/src/routes/pricing.tsx (4)

702-723: Good implementation of region-gating user feedback

The addition of a clear warning message for users in unsupported App Store regions is excellent. It provides helpful context about why certain features are unavailable and displays the detected region, which is valuable for support and debugging.


151-154: Well-structured region-gating implementation

The approach to detecting the region and gating external billing is well-implemented:

  1. Proper state initialization with safe defaults
  2. Dynamic importing of region-gating utilities
  3. Error handling that defaults to a secure state (blocking external billing on error)
  4. Setting the region for UI display

This creates a robust system that safely handles region detection and properly restricts functionality when needed.

Also applies to: 169-177, 179-183


455-458: Early return pattern is appropriate with new warning UI

The early return for unsupported regions is now appropriate since you've added a clear warning message (lines 702-723) that informs users why the feature is unavailable. This is a good pattern when combined with proper user feedback.


837-845: Complete implementation of button disabling

The button disabling logic for paid plans when external billing is not allowed is implemented correctly. You've covered all three necessary conditions: during loading, for Bitcoin team plans, and for iOS devices in non-US regions.

Comment on lines 24 to 48
export async function allowExternalBilling(): Promise<boolean> {
const regionCode = await getStoreRegion();
const isAllowed = US_CODES.includes(regionCode);
console.log("[Region Gate] Is US region:", isAllowed, "Valid US codes:", US_CODES);
return isAllowed;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Normalise the region code before lookup
getStoreRegion() may return codes in lower-case or mixed-case depending on platform implementation. Convert to upper-case before comparison to avoid false negatives.

-  const regionCode = await getStoreRegion();
+  const regionCode = (await getStoreRegion()).toUpperCase();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function allowExternalBilling(): Promise<boolean> {
const regionCode = await getStoreRegion();
const isAllowed = US_CODES.includes(regionCode);
console.log("[Region Gate] Is US region:", isAllowed, "Valid US codes:", US_CODES);
return isAllowed;
}
export async function allowExternalBilling(): Promise<boolean> {
const regionCode = (await getStoreRegion()).toUpperCase();
const isAllowed = US_CODES.includes(regionCode);
console.log("[Region Gate] Is US region:", isAllowed, "Valid US codes:", US_CODES);
return isAllowed;
}

@AnthonyRonning AnthonyRonning force-pushed the apple-us-only-external branch from a023139 to 06fb8a9 Compare May 5, 2025 22:13
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (7)
plugins/store/.tauri/tauri-api/Sources/Tauri/Invoke.swift (4)

33-38: ⚠️ Potential issue

Avoid force unwrapping and casting in getArgs()

The current implementation has multiple force unwraps and casts that could crash with invalid input.

-  public func getArgs() throws -> JSObject {
-    let jsonData = self.data.data(using: .utf8)!
-    let data = try JSONSerialization.jsonObject(with: jsonData, options: [])
-    return JSTypes.coerceDictionaryToJSObject(
-      (data as! NSDictionary), formattingDatesAsStrings: true)!
+  public func getArgs() throws -> JSObject {
+    guard let jsonData = self.data.data(using: .utf8) else {
+      throw NSError(domain: "Invoke", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid UTF-8 data"])
+    }
+    let data = try JSONSerialization.jsonObject(with: jsonData, options: [])
+    guard 
+      let dict = data as? NSDictionary,
+      let result = JSTypes.coerceDictionaryToJSObject(dict, formattingDatesAsStrings: true)
+    else {
+      throw NSError(domain: "Invoke", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to coerce data to JSObject"])
+    }
+    return result
🧰 Tools
🪛 SwiftLint (0.57.0)

[Error] 37-37: Force casts should be avoided

(force_cast)


40-45: ⚠️ Potential issue

Avoid force unwrapping in parseArgs<T>()

Similar to getArgs(), this method also force unwraps the UTF-8 data which could cause crashes.

-  public func parseArgs<T: Decodable>(_ type: T.Type) throws -> T {
-    let jsonData = self.data.data(using: .utf8)!
+  public func parseArgs<T: Decodable>(_ type: T.Type) throws -> T {
+    guard let jsonData = self.data.data(using: .utf8) else {
+      throw NSError(domain: "Invoke", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid UTF-8 data"])
+    }

67-74: ⚠️ Potential issue

Avoid lossy String(decoding:as:) conversion

Prefer the failable String(data:encoding:) initializer so you can throw or reject when the data is not valid UTF-8.

-      let json = try JSONEncoder().encode(data)
-      sendResponse(callback, String(decoding: json, as: UTF8.self))
+      let json = try JSONEncoder().encode(data)
+      guard let jsonString = String(data: json, encoding: .utf8) else {
+        throw NSError(domain: "Invoke", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid UTF-8"])
+      }
+      sendResponse(callback, jsonString)
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 70-70: Prefer failable String(data:encoding:) initializer when converting Data to String

(optional_data_string_conversion)


99-100: ⚠️ Potential issue

Avoid force casting in reject() method

Force casting payload as! JsonObject could crash if the payload doesn't conform to the expected type.

-    sendResponse(self.error, serialize(.dictionary(payload as! JsonObject)))
+    guard let jsonObject = payload as? JsonObject else {
+      sendResponse(self.error, serialize(.string("Failed to convert payload to JsonObject")))
+      return
+    }
+    sendResponse(self.error, serialize(.dictionary(jsonObject)))
🧰 Tools
🪛 SwiftLint (0.57.0)

[Error] 100-100: Force casts should be avoided

(force_cast)

plugins/store/.tauri/tauri-api/Sources/Tauri/Tauri.swift (3)

33-39: Replace force unwrapping with safe URL initialization

Force unwrapping the URL with URL(string: "asset://localhost")! will crash if the string is ever malformed or changes. This should be handled safely.

-    return URL(string: "asset://localhost")!.appendingPathComponent(inputURL.path)
+    guard let base = URL(string: "asset://localhost") else {
+      return nil
+    }
+    return base.appendingPathComponent(inputURL.path)

59-76: Address multiple issues in dynamic invocation block

The dynamic invocation implementation contains several issues:

  1. Redundant optional initialization (var error: NSError? = nil)
  2. Force-unwrapping of the method IMP
  3. The TODO comment about a deliberate memory leak to prevent crashes
-          var error: NSError? = nil
+          var error: NSError?
           withUnsafeMutablePointer(to: &error) {
-            let methodIMP: IMP! = plugin.instance.method(for: selectorWithThrows)
+            guard let methodIMP = plugin.instance.method(for: selectorWithThrows) else {
+              invoke.reject("Failed to get method implementation for \(invoke.command)")
+              return
+            }
             unsafeBitCast(
               methodIMP, to: (@convention(c) (Any?, Selector, Invoke, OpaquePointer) -> Void).self)(
                 plugin.instance, selectorWithThrows, invoke, OpaquePointer($0))
           }
           if let error = error {
             invoke.reject("\(error)")
-            // TODO: app crashes without this leak
-            let _ = Unmanaged.passRetained(error)
+            // Fix the underlying memory management issue instead of leaking
+            // One approach could be to properly transfer ownership using Unmanaged.passUnretained(error).toOpaque()
           }
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 64-64: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)


[Warning] 73-73: TODOs should be resolved (app crashes without this leak)

(todo)


97-105: Replace force-cast with safe type check when registering plugins

Force-casting to Plugin will crash the app if the object doesn't conform to the protocol.

-func registerPlugin(name: SRString, plugin: NSObject, config: SRString, webview: WKWebView?) {
-  PluginManager.shared.load(
-    name: name.toString(),
-    plugin: plugin as! Plugin,
-    config: config.toString(),
-    webview: webview
-  )
+func registerPlugin(
+  name: SRString,
+  plugin: NSObject,
+  config: SRString,
+  webview: WKWebView?
+) {
+  guard let typed = plugin as? Plugin else {
+    os_log("Attempted to register an object that does not conform to Plugin", type: .fault)
+    return
+  }
+  PluginManager.shared.load(
+    name: name.toString(),
+    plugin: typed,
+    config: config.toString(),
+    webview: webview
+  )
 }
🧰 Tools
🪛 SwiftLint (0.57.0)

[Error] 101-101: Force casts should be avoided

(force_cast)

🧹 Nitpick comments (2)
plugins/store/.tauri/tauri-api/Sources/Tauri/Tauri.swift (2)

41-47: Use where clause instead of nested if in for loop

The for loop can be simplified using a where clause for better readability.

-  func onWebviewCreated(_ webview: WKWebView) {
-    for (_, handle) in plugins {
-      if !handle.loaded {
-        handle.instance.load(webview: webview)
-      }
-    }
-  }
+  func onWebviewCreated(_ webview: WKWebView) {
+    for (_, handle) in plugins where !handle.loaded {
+      handle.instance.load(webview: webview)
+    }
+  }
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 43-43: where clauses are preferred over a single if inside a for

(for_where)


124-133: Remove redundant Void return type in closures

The explicit return type of Void in closure declarations is redundant. Simplify these declarations for better readability.

  let invoke = Invoke(
    command: command.toString(), callback: callbackId, error: errorId,
-    sendResponse: { (fn: UInt64, payload: String?) -> Void in
+    sendResponse: { (fn: UInt64, payload: String?) in
      let success = fn == callbackId
      callback(id, success, payload ?? "null")
    },
-    sendChannelData: { (id: UInt64, payload: String) -> Void in
+    sendChannelData: { (id: UInt64, payload: String) in
      sendChannelData(id, payload)
    }, data: data.toString())
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 126-126: Returning Void in a function declaration is redundant

(redundant_void_return)


[Warning] 130-130: Returning Void in a function declaration is redundant

(redundant_void_return)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a023139 and 06fb8a9.

⛔ Files ignored due to path filters (2)
  • frontend/src-tauri/Cargo.lock is excluded by !**/*.lock
  • plugins/store/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (32)
  • .gitignore (1 hunks)
  • flake.nix (1 hunks)
  • frontend/src-tauri/Cargo.toml (1 hunks)
  • frontend/src-tauri/src/lib.rs (2 hunks)
  • frontend/src/components/Marketing.tsx (8 hunks)
  • frontend/src/routes/auth.$provider.callback.tsx (1 hunks)
  • frontend/src/routes/pricing.tsx (9 hunks)
  • frontend/src/utils/region-gate.ts (1 hunks)
  • plugins/store/.tauri/tauri-api/.gitignore (1 hunks)
  • plugins/store/.tauri/tauri-api/Package.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/README.md (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Channel.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Invoke.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/JSTypes.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/JsonValue.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Logger.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Plugin/Plugin.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Tauri.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/UiUtils.swift (1 hunks)
  • plugins/store/Cargo.toml (1 hunks)
  • plugins/store/build.rs (1 hunks)
  • plugins/store/ios/Package.resolved (1 hunks)
  • plugins/store/ios/Package.swift (1 hunks)
  • plugins/store/ios/Sources/StorePlugin.swift (1 hunks)
  • plugins/store/permissions/autogenerated/commands/get_region.toml (1 hunks)
  • plugins/store/permissions/autogenerated/reference.md (1 hunks)
  • plugins/store/permissions/schemas/schema.json (1 hunks)
  • plugins/store/src/commands.rs (1 hunks)
  • plugins/store/src/desktop.rs (1 hunks)
  • plugins/store/src/error.rs (1 hunks)
  • plugins/store/src/lib.rs (1 hunks)
  • plugins/store/src/mobile.rs (1 hunks)
✅ Files skipped from review due to trivial changes (3)
  • plugins/store/ios/Package.resolved
  • plugins/store/permissions/autogenerated/reference.md
  • plugins/store/permissions/schemas/schema.json
🚧 Files skipped from review as they are similar to previous changes (24)
  • plugins/store/.tauri/tauri-api/README.md
  • plugins/store/build.rs
  • flake.nix
  • .gitignore
  • frontend/src-tauri/Cargo.toml
  • frontend/src-tauri/src/lib.rs
  • plugins/store/ios/Package.swift
  • plugins/store/permissions/autogenerated/commands/get_region.toml
  • plugins/store/.tauri/tauri-api/.gitignore
  • plugins/store/.tauri/tauri-api/Sources/Tauri/UiUtils.swift
  • plugins/store/Cargo.toml
  • plugins/store/src/commands.rs
  • plugins/store/src/lib.rs
  • plugins/store/src/error.rs
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Logger.swift
  • plugins/store/.tauri/tauri-api/Sources/Tauri/JsonValue.swift
  • plugins/store/src/mobile.rs
  • plugins/store/src/desktop.rs
  • plugins/store/.tauri/tauri-api/Package.swift
  • frontend/src/utils/region-gate.ts
  • frontend/src/routes/auth.$provider.callback.tsx
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Plugin/Plugin.swift
  • frontend/src/routes/pricing.tsx
  • frontend/src/components/Marketing.tsx
🧰 Additional context used
🧬 Code Graph Analysis (1)
plugins/store/ios/Sources/StorePlugin.swift (2)
plugins/store/.tauri/tauri-api/Sources/Tauri/Tauri.swift (1)
  • invoke (59-88)
plugins/store/.tauri/tauri-api/Sources/Tauri/Invoke.swift (4)
  • resolve (55-57)
  • resolve (59-61)
  • resolve (63-65)
  • resolve (67-74)
🪛 SwiftLint (0.57.0)
plugins/store/.tauri/tauri-api/Sources/Tauri/Tauri.swift

[Warning] 43-43: where clauses are preferred over a single if inside a for

(for_where)


[Error] 101-101: Force casts should be avoided

(force_cast)


[Warning] 64-64: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)


[Warning] 126-126: Returning Void in a function declaration is redundant

(redundant_void_return)


[Warning] 130-130: Returning Void in a function declaration is redundant

(redundant_void_return)


[Warning] 73-73: TODOs should be resolved (app crashes without this leak)

(todo)

plugins/store/.tauri/tauri-api/Sources/Tauri/Channel.swift

[Warning] 62-62: Prefer failable String(data:encoding:) initializer when converting Data to String

(optional_data_string_conversion)

plugins/store/.tauri/tauri-api/Sources/Tauri/Invoke.swift

[Error] 37-37: Force casts should be avoided

(force_cast)


[Error] 100-100: Force casts should be avoided

(force_cast)


[Warning] 70-70: Prefer failable String(data:encoding:) initializer when converting Data to String

(optional_data_string_conversion)

⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: build-linux
  • GitHub Check: build-macos (universal-apple-darwin)
  • GitHub Check: build-ios
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (6)
plugins/store/ios/Sources/StorePlugin.swift (3)

9-35: LGTM: Good implementation of region detection with fallback mechanism

The implementation of getRegion properly handles different iOS versions with an appropriate fallback mechanism. It first tries StoreKit 2 API (iOS 15+), then falls back to StoreKit 1, and finally to device locale when all else fails.


37-59: LGTM: Well-structured fallback implementation with multiple levels

The fallback method is well-structured with good logging at each step, making it easy to debug issues. The cascading approach from StoreKit 1 to device locale to a hardcoded "UNKNOWN" value ensures resilience.


62-65: LGTM: Clean plugin initialization

The C-exported function initPlugin provides a clean entry point for the native plugin system.

plugins/store/.tauri/tauri-api/Sources/Tauri/JSTypes.swift (3)

7-19: LGTM: Good protocol design for JavaScript compatibility

The JSValue protocol design provides a clean way to represent JavaScript-compatible value types in Swift. The extensions to common Swift types ensure broad compatibility.


24-61: LGTM: Type-safe accessor methods

The dictionary extension provides convenient type-safe accessor methods that make it easy to extract typed values from JSON objects. The methods handle nil values appropriately.


87-123: LGTM: Comprehensive type conversion

The coerceToJSValue function provides comprehensive type conversion with good handling of various Swift and Foundation types. The recursive processing of arrays and dictionaries ensures deep conversion.

Comment on lines +61 to +62
let json = try JSONEncoder().encode(data)
handler(id, String(decoding: json, as: UTF8.self))
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix potential crash in String conversion

The current implementation uses String(decoding:as:) which can crash if the data isn't valid UTF-8. Use the failable String(data:encoding:) initializer instead and handle the failure case.

-  public func send<T: Encodable>(_ data: T) throws {
-    let json = try JSONEncoder().encode(data)
-    handler(id, String(decoding: json, as: UTF8.self))
+  public func send<T: Encodable>(_ data: T) throws {
+    let json = try JSONEncoder().encode(data)
+    guard let jsonString = String(data: json, encoding: .utf8) else {
+      throw NSError(domain: "Channel", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to encode data as UTF-8"])
+    }
+    handler(id, jsonString)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let json = try JSONEncoder().encode(data)
handler(id, String(decoding: json, as: UTF8.self))
public func send<T: Encodable>(_ data: T) throws {
let json = try JSONEncoder().encode(data)
guard let jsonString = String(data: json, encoding: .utf8) else {
throw NSError(
domain: "Channel",
code: 0,
userInfo: [NSLocalizedDescriptionKey: "Failed to encode data as UTF-8"]
)
}
handler(id, jsonString)
}
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 62-62: Prefer failable String(data:encoding:) initializer when converting Data to String

(optional_data_string_conversion)

@AnthonyRonning AnthonyRonning force-pushed the apple-us-only-external branch 3 times, most recently from 77cacfc to 89f84b7 Compare May 5, 2025 23:11
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (10)
frontend/src/utils/region-gate.ts (2)

3-3: Consider using an enum or TypeScript type for region codes

Using a TypeScript enum or type for region codes would improve type safety and prevent typos.

-const US_CODES = ["USA", "ASM", "GUM", "PRI", "VIR", "MNP", "UMI"]; // US and territories
+enum RegionCode {
+  USA = "USA", // United States
+  ASM = "ASM", // American Samoa
+  GUM = "GUM", // Guam
+  PRI = "PRI", // Puerto Rico
+  VIR = "VIR", // U.S. Virgin Islands
+  MNP = "MNP", // Northern Mariana Islands
+  UMI = "UMI", // U.S. Minor Outlying Islands
+}
+
+const US_CODES = Object.values(RegionCode);

43-48: Normalize the region code before lookup

getStoreRegion() may return codes in lower-case or mixed-case depending on platform implementation. Convert to upper-case before comparison to avoid false negatives.

export async function allowExternalBilling(): Promise<boolean> {
-  const regionCode = await getStoreRegion();
+  const regionCode = (await getStoreRegion()).toUpperCase();
  const isAllowed = US_CODES.includes(regionCode);
  console.log("[Region Gate] Is US region:", isAllowed, "Valid US codes:", US_CODES);
  return isAllowed;
}
plugins/store/.tauri/tauri-api/Sources/Tauri/Channel.swift (1)

61-62: ⚠️ Potential issue

Fix potential crash in String conversion

The current implementation uses String(decoding:as:) which can crash if the data isn't valid UTF-8. Use the failable String(data:encoding:) initializer instead and handle the failure case.

-  public func send<T: Encodable>(_ data: T) throws {
-    let json = try JSONEncoder().encode(data)
-    handler(id, String(decoding: json, as: UTF8.self))
+  public func send<T: Encodable>(_ data: T) throws {
+    let json = try JSONEncoder().encode(data)
+    guard let jsonString = String(data: json, encoding: .utf8) else {
+      throw NSError(domain: "Channel", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to encode data as UTF-8"])
+    }
+    handler(id, jsonString)
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 62-62: Prefer failable String(data:encoding:) initializer when converting Data to String

(optional_data_string_conversion)

plugins/store/.tauri/tauri-api/Sources/Tauri/Invoke.swift (4)

34-38: ⚠️ Potential issue

Remove force-cast when coercing JSON to JSObject

as! NSDictionary will crash on malformed payloads. Use optional binding and surface an error through reject.

-    let jsonData = self.data.data(using: .utf8)!
-    let data = try JSONSerialization.jsonObject(with: jsonData, options: [])
-    return JSTypes.coerceDictionaryToJSObject(
-      (data as! NSDictionary), formattingDatesAsStrings: true)!
+    guard let jsonData = self.data.data(using: .utf8) else {
+      throw NSError(domain: "Invoke", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid UTF-8 data"])
+    }
+    let data = try JSONSerialization.jsonObject(with: jsonData, options: [])
+    guard
+      let dict = data as? NSDictionary,
+      let js = JSTypes.coerceDictionaryToJSObject(dict, formattingDatesAsStrings: true)
+    else {
+      throw NSError(domain: "Invoke", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid arguments"])
+    }
+    return js
🧰 Tools
🪛 SwiftLint (0.57.0)

[Error] 37-37: Force casts should be avoided

(force_cast)


40-45: ⚠️ Potential issue

Avoid force unwrapping in parseArgs<T>().

Similar to getArgs(), this method also force unwraps the UTF-8 data which could cause crashes.

  public func parseArgs<T: Decodable>(_ type: T.Type) throws -> T {
-    let jsonData = self.data.data(using: .utf8)!
+    guard let jsonData = self.data.data(using: .utf8) else {
+      throw NSError(domain: "Invoke", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid UTF-8 data"])
+    }
    let decoder = JSONDecoder()
    decoder.userInfo[channelDataKey] = sendChannelData
    return try decoder.decode(type, from: jsonData)
  }

67-72: ⚠️ Potential issue

Avoid lossy String(decoding:as:) conversion

Prefer the failable String(data:encoding:) initialiser so you can throw or reject when the data is not valid UTF-8.

-      let json = try JSONEncoder().encode(data)
-      sendResponse(callback, String(decoding: json, as: UTF8.self))
+      let json = try JSONEncoder().encode(data)
+      guard let jsonString = String(data: json, encoding: .utf8) else {
+        throw NSError(domain: "Invoke", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid UTF-8"])
+      }
+      sendResponse(callback, jsonString)
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 70-70: Prefer failable String(data:encoding:) initializer when converting Data to String

(optional_data_string_conversion)


99-100: ⚠️ Potential issue

Avoid force casting in reject() method.

Force casting payload as! JsonObject could crash if the payload doesn't conform to the expected type.

-    sendResponse(self.error, serialize(.dictionary(payload as! JsonObject)))
+    guard let jsonObject = payload as? JsonObject else {
+      sendResponse(self.error, serialize(.string("Failed to convert payload to JsonObject")))
+      return
+    }
+    sendResponse(self.error, serialize(.dictionary(jsonObject)))
🧰 Tools
🪛 SwiftLint (0.57.0)

[Error] 100-100: Force casts should be avoided

(force_cast)

plugins/store/.tauri/tauri-api/Sources/Tauri/Tauri.swift (3)

33-39: ⚠️ Potential issue

Avoid force-unwrapping when composing the asset URL

URL(string: "asset://localhost")! will crash the app if the hard-coded string ever changes or is malformed. Using a failable initializer keeps the app resilient.

-    return URL(string: "asset://localhost")!.appendingPathComponent(inputURL.path)
+    guard let base = URL(string: "asset://localhost") else {
+      return nil
+    }
+    return base.appendingPathComponent(inputURL.path)

59-76: ⚠️ Potential issue

Multiple issues in dynamic invocation block

  1. Redundant optional initialization (var error: NSError? = nil) – just declare var error: NSError?.
  2. Force-casting the method IMP signature is brittle; consider moving to @objc protocol & performSelector with typed wrappers where feasible.
  3. The "TODO: app crashes without this leak" comment indicates a deliberate leak (passRetained). Pinpoint and fix the real life-cycle problem instead of leaking objects.
-          var error: NSError? = nil
+          var error: NSError?
...
-            // TODO: app crashes without this leak
-            let _ = Unmanaged.passRetained(error)
+            // Passing retained object hides the underlying ARC issue.
+            // Investigate why the NSError is over-released instead of leaking it.
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 64-64: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)


[Warning] 73-73: TODOs should be resolved (app crashes without this leak)

(todo)


97-104: ⚠️ Potential issue

Replace force-cast with safe down-cast when registering plugins

If a non-conforming NSObject is accidentally registered, the entire process crashes. Fail early and clearly instead.

-func registerPlugin(name: SRString, plugin: NSObject, config: SRString, webview: WKWebView?) {
-  PluginManager.shared.load(
-    name: name.toString(),
-    plugin: plugin as! Plugin,
-    config: config.toString(),
-    webview: webview
-  )
+func registerPlugin(
+  name: SRString,
+  plugin: NSObject,
+  config: SRString,
+  webview: WKWebView?
+) {
+  guard let typed = plugin as? Plugin else {
+    os_log("Attempted to register an object that does not conform to Plugin", type: .fault)
+    return
+  }
+  PluginManager.shared.load(
+    name: name.toString(),
+    plugin: typed,
+    config: config.toString(),
+    webview: webview
+  )
 }
🧰 Tools
🪛 SwiftLint (0.57.0)

[Error] 101-101: Force casts should be avoided

(force_cast)

🧹 Nitpick comments (5)
frontend/src/utils/region-gate.ts (3)

31-33: Consider increasing the error message length limit

The current 30-character limit for error messages might truncate important debugging information. Consider increasing this limit or conditionally truncating only in production environments.

-    // Truncate if the error message is too long
-    if (errorMsg.length > 30) {
-      errorMsg = errorMsg.substring(0, 27) + "...";
-    }
+    // Truncate if the error message is too long
+    if (errorMsg.length > 100) {
+      errorMsg = errorMsg.substring(0, 97) + "...";
+    }

10-37: Consider handling the "UNKNOWN" region code case explicitly

The comment mentions that the Swift code returns "UNKNOWN" if it can't determine the region, but there's no special handling for this case. Consider explicitly checking for this value.

export async function getStoreRegion(): Promise<string> {
  console.log("[Region Gate] Attempting to invoke plugin:store|get_region...");
  try {
    const code = await invoke<string>("plugin:store|get_region");
    console.log("[Region Gate] Success! Store region code:", code);
+    // Check if we got the fallback value from Swift
+    if (code === "UNKNOWN") {
+      console.warn("[Region Gate] Region could not be determined by the native code");
+    }
    return code;
  } catch (error) {
    // Error handling remains unchanged

1-56: Add unit tests for region gating logic

Consider adding unit tests for this module to verify the behavior of the region gating logic, especially for edge cases like unknown regions, error conditions, and case sensitivity.

// Example test structure
describe('Region Gate', () => {
  beforeEach(() => {
    // Mock invoke function
    vi.mock('@tauri-apps/api/core', () => ({
      invoke: vi.fn()
    }));
  });
  
  test('allowExternalBilling returns true for US regions', async () => {
    // Test implementation
  });
  
  test('allowExternalBilling returns false for non-US regions', async () => {
    // Test implementation
  });
  
  test('allowExternalBilling handles errors gracefully', async () => {
    // Test implementation
  });
});
plugins/store/.tauri/tauri-api/Sources/Tauri/Tauri.swift (2)

41-47: Use for-where clause for better readability and efficiency

Replace the if statement inside the for loop with a where clause for better Swift style.

-  func onWebviewCreated(_ webview: WKWebView) {
-    for (_, handle) in plugins {
-      if !handle.loaded {
-        handle.instance.load(webview: webview)
-      }
-    }
-  }
+  func onWebviewCreated(_ webview: WKWebView) {
+    for (_, handle) in plugins where !handle.loaded {
+      handle.instance.load(webview: webview)
+    }
+  }
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 43-43: where clauses are preferred over a single if inside a for

(for_where)


126-129: Remove redundant Void return type annotations

The -> Void annotations in closure parameter types are redundant and can be removed.

-    sendResponse: { (fn: UInt64, payload: String?) -> Void in
+    sendResponse: { (fn: UInt64, payload: String?) in
      let success = fn == callbackId
      callback(id, success, payload ?? "null")
    },
-    sendChannelData: { (id: UInt64, payload: String) -> Void in
+    sendChannelData: { (id: UInt64, payload: String) in
      sendChannelData(id, payload)
    }, data: data.toString())

Also applies to: 130-132

🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 126-126: Returning Void in a function declaration is redundant

(redundant_void_return)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 77cacfc and 89f84b7.

⛔ Files ignored due to path filters (2)
  • frontend/src-tauri/Cargo.lock is excluded by !**/*.lock
  • plugins/store/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (34)
  • .gitignore (1 hunks)
  • flake.nix (1 hunks)
  • frontend/src-tauri/Cargo.toml (1 hunks)
  • frontend/src-tauri/capabilities/default.json (1 hunks)
  • frontend/src-tauri/capabilities/mobile-ios.json (1 hunks)
  • frontend/src-tauri/src/lib.rs (2 hunks)
  • frontend/src/components/Marketing.tsx (8 hunks)
  • frontend/src/routes/auth.$provider.callback.tsx (1 hunks)
  • frontend/src/routes/pricing.tsx (9 hunks)
  • frontend/src/utils/region-gate.ts (1 hunks)
  • plugins/store/.tauri/tauri-api/.gitignore (1 hunks)
  • plugins/store/.tauri/tauri-api/Package.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/README.md (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Channel.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Invoke.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/JSTypes.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/JsonValue.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Logger.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Plugin/Plugin.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Tauri.swift (1 hunks)
  • plugins/store/.tauri/tauri-api/Sources/Tauri/UiUtils.swift (1 hunks)
  • plugins/store/Cargo.toml (1 hunks)
  • plugins/store/build.rs (1 hunks)
  • plugins/store/ios/Package.resolved (1 hunks)
  • plugins/store/ios/Package.swift (1 hunks)
  • plugins/store/ios/Sources/StorePlugin.swift (1 hunks)
  • plugins/store/permissions/autogenerated/commands/get_region.toml (1 hunks)
  • plugins/store/permissions/autogenerated/reference.md (1 hunks)
  • plugins/store/permissions/schemas/schema.json (1 hunks)
  • plugins/store/src/commands.rs (1 hunks)
  • plugins/store/src/desktop.rs (1 hunks)
  • plugins/store/src/error.rs (1 hunks)
  • plugins/store/src/lib.rs (1 hunks)
  • plugins/store/src/mobile.rs (1 hunks)
✅ Files skipped from review due to trivial changes (3)
  • plugins/store/permissions/autogenerated/reference.md
  • plugins/store/ios/Package.swift
  • plugins/store/Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (27)
  • plugins/store/.tauri/tauri-api/README.md
  • plugins/store/build.rs
  • frontend/src-tauri/Cargo.toml
  • frontend/src/routes/auth.$provider.callback.tsx
  • flake.nix
  • frontend/src-tauri/src/lib.rs
  • .gitignore
  • frontend/src-tauri/capabilities/default.json
  • frontend/src-tauri/capabilities/mobile-ios.json
  • plugins/store/permissions/autogenerated/commands/get_region.toml
  • plugins/store/src/commands.rs
  • plugins/store/ios/Package.resolved
  • plugins/store/src/desktop.rs
  • plugins/store/.tauri/tauri-api/.gitignore
  • plugins/store/.tauri/tauri-api/Package.swift
  • plugins/store/src/mobile.rs
  • plugins/store/.tauri/tauri-api/Sources/Tauri/JsonValue.swift
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Logger.swift
  • plugins/store/ios/Sources/StorePlugin.swift
  • plugins/store/src/lib.rs
  • frontend/src/components/Marketing.tsx
  • plugins/store/.tauri/tauri-api/Sources/Tauri/Plugin/Plugin.swift
  • plugins/store/src/error.rs
  • plugins/store/.tauri/tauri-api/Sources/Tauri/UiUtils.swift
  • plugins/store/permissions/schemas/schema.json
  • plugins/store/.tauri/tauri-api/Sources/Tauri/JSTypes.swift
  • frontend/src/routes/pricing.tsx
🧰 Additional context used
🧬 Code Graph Analysis (1)
frontend/src/utils/region-gate.ts (2)
plugins/store/.tauri/tauri-api/Sources/Tauri/Tauri.swift (1)
  • invoke (59-88)
plugins/store/.tauri/tauri-api/Sources/Tauri/Logger.swift (1)
  • error (55-57)
🪛 SwiftLint (0.57.0)
plugins/store/.tauri/tauri-api/Sources/Tauri/Tauri.swift

[Warning] 43-43: where clauses are preferred over a single if inside a for

(for_where)


[Error] 101-101: Force casts should be avoided

(force_cast)


[Warning] 64-64: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)


[Warning] 126-126: Returning Void in a function declaration is redundant

(redundant_void_return)


[Warning] 130-130: Returning Void in a function declaration is redundant

(redundant_void_return)


[Warning] 73-73: TODOs should be resolved (app crashes without this leak)

(todo)

plugins/store/.tauri/tauri-api/Sources/Tauri/Channel.swift

[Warning] 62-62: Prefer failable String(data:encoding:) initializer when converting Data to String

(optional_data_string_conversion)

plugins/store/.tauri/tauri-api/Sources/Tauri/Invoke.swift

[Error] 37-37: Force casts should be avoided

(force_cast)


[Error] 100-100: Force casts should be avoided

(force_cast)


[Warning] 70-70: Prefer failable String(data:encoding:) initializer when converting Data to String

(optional_data_string_conversion)

⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: build-linux
  • GitHub Check: build-macos (universal-apple-darwin)
  • GitHub Check: build-ios
  • GitHub Check: Cloudflare Pages

Comment on lines +53 to +55
export function isUSRegion(regionCode: string): boolean {
return US_CODES.includes(regionCode);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Normalize the region code in isUSRegion too

For consistency with the recommendation for allowExternalBilling(), also normalize the region code in this function.

export function isUSRegion(regionCode: string): boolean {
-  return US_CODES.includes(regionCode);
+  return US_CODES.includes(regionCode.toUpperCase());
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function isUSRegion(regionCode: string): boolean {
return US_CODES.includes(regionCode);
}
export function isUSRegion(regionCode: string): boolean {
return US_CODES.includes(regionCode.toUpperCase());
}

@AnthonyRonning AnthonyRonning force-pushed the apple-us-only-external branch from 89f84b7 to ef01fa5 Compare May 8, 2025 19:11
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (2)
frontend/src/routes/pricing.tsx (2)

209-209: Remove console.log statement before production.

This debug statement should be removed before deploying to production.

-              console.log("Store region:", region, "External billing allowed:", allowed);
+              setExternalBillingAllowed(allowed);

486-489: 🛠️ Refactor suggestion

Add user feedback when external billing is not allowed.

When external billing is not allowed for iOS users outside the US, the code silently blocks actions without providing immediate feedback to the user.

Consider showing a toast or message to inform the user why the action isn't available instead of silently returning. This improves user experience by providing immediate feedback.

if (isIOS && !product.name.toLowerCase().includes("free") && !externalBillingAllowed) {
  // Don't allow any action on paid plans if on iOS and external billing not allowed
+  // Show toast or notification to user explaining why
+  toast({
+    title: "Not available in your region",
+    description: "Direct purchases are only available for US App Store accounts.",
+    variant: "destructive"
+  });
  return;
}
🧹 Nitpick comments (5)
frontend/src/components/ApplePayButton.tsx (1)

31-45: Consider handling edge cases in product loading

The product loading logic looks good, but there's no feedback to the user when product information can't be loaded.

Consider adding a fallback state when product info fails to load:

  useEffect(() => {
    async function loadProductInfo() {
      try {
        const products = await applePayService.getProducts([productId]);
        if (products && products.length > 0) {
          const product = products[0];
          setButtonLabel(`${text} ${product.price}`);
+       } else {
+         console.warn("No products found for given ID");
+         setButtonLabel(text); // Fallback to default text
        }
      } catch (error) {
        console.error("Error loading product info:", error);
+       setButtonLabel(text); // Fallback to default text on error
      }
    }
    
    loadProductInfo();
  }, [productId, text]);
frontend/src/billing/billingService.ts (1)

144-146: Method naming could be improved for clarity.

The method isApplePayRequired() simply returns the result of shouldShowApplePay(), but the names imply different behaviors, which may confuse developers.

Consider renaming or implementing distinct logic to differentiate these methods:

-  async isApplePayRequired(): Promise<boolean> {
-    return this.shouldShowApplePay();
+  async isApplePayRequired(): Promise<boolean> {
+    // Make it clear this is specifically checking if Apple Pay is the ONLY option
+    try {
+      const { type } = await import("@tauri-apps/plugin-os");
+      const platform = await type();
+      
+      if (platform !== "ios") {
+        return false;
+      }
+      
+      return !(await allowExternalBilling());
+    } catch (error) {
+      console.error("Error checking if Apple Pay is required:", error);
+      return false;
+    }
plugins/store/ios/Sources/StorePlugin.swift (1)

21-21: Redundant nil initialization.

The transactionListenerTask variable is initialized with nil, which is redundant for optional properties.

- private var transactionListenerTask: Task<Void, Error>? = nil
+ private var transactionListenerTask: Task<Void, Error>?
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 21-21: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)

plugins/store/src/mobile.rs (2)

55-72: Consider adding validation for product data.

The Product struct has good field definitions, but since it's receiving data from external APIs, consider adding validation for required fields or handling default values more explicitly.

You might want to implement the Default trait for this struct to provide sensible defaults for optional fields:

impl Default for Product {
    fn default() -> Self {
        Self {
            id: String::new(),
            title: String::new(),
            description: String::new(),
            price: String::new(),
            price_value: 0.0,
            currency_code: String::new(),
            r#type: String::new(),
            subscription_period: None,
            introductory_offer: None,
            promotional_offers: None,
        }
    }
}

161-165: Implement caching for store region.

The get_region method makes a plugin call each time it's invoked. Consider implementing caching for the region value, as this is unlikely to change during a session.

You could implement caching using a lazy static or by storing the region in the Store struct after the first call:

pub struct Store<R: Runtime> {
    handle: PluginHandle<R>,
    cached_region: std::sync::Mutex<Option<String>>,
}

// Then in get_region:
pub fn get_region(&self) -> crate::Result<String> {
    // Check cache first
    if let Some(region) = self.cached_region.lock().unwrap().as_ref() {
        return Ok(region.clone());
    }
    
    // Otherwise make the call
    let region = self.handle
        .run_mobile_plugin("getRegion", ())
        .map_err(Into::into)?;
        
    // Cache the result
    *self.cached_region.lock().unwrap() = Some(region.clone());
    
    Ok(region)
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5784d2b and ede3f69.

📒 Files selected for processing (21)
  • frontend/src-tauri/capabilities/default.json (1 hunks)
  • frontend/src-tauri/capabilities/mobile-ios.json (1 hunks)
  • frontend/src/billing/applePayService.ts (1 hunks)
  • frontend/src/billing/billingApi.ts (1 hunks)
  • frontend/src/billing/billingService.ts (3 hunks)
  • frontend/src/components/ApplePayButton.tsx (1 hunks)
  • frontend/src/routes/pricing.tsx (10 hunks)
  • frontend/src/utils/region-gate.ts (1 hunks)
  • plugins/store/build.rs (1 hunks)
  • plugins/store/ios/Sources/StorePlugin.swift (1 hunks)
  • plugins/store/permissions/autogenerated/commands/get_products.toml (1 hunks)
  • plugins/store/permissions/autogenerated/commands/get_subscription_status.toml (1 hunks)
  • plugins/store/permissions/autogenerated/commands/get_transactions.toml (1 hunks)
  • plugins/store/permissions/autogenerated/commands/purchase.toml (1 hunks)
  • plugins/store/permissions/autogenerated/commands/restore_purchases.toml (1 hunks)
  • plugins/store/permissions/autogenerated/commands/verify_purchase.toml (1 hunks)
  • plugins/store/permissions/autogenerated/reference.md (1 hunks)
  • plugins/store/permissions/schemas/schema.json (1 hunks)
  • plugins/store/src/commands.rs (1 hunks)
  • plugins/store/src/lib.rs (1 hunks)
  • plugins/store/src/mobile.rs (1 hunks)
✅ Files skipped from review due to trivial changes (6)
  • plugins/store/permissions/autogenerated/commands/purchase.toml
  • plugins/store/permissions/autogenerated/commands/verify_purchase.toml
  • plugins/store/permissions/autogenerated/commands/restore_purchases.toml
  • plugins/store/permissions/autogenerated/commands/get_transactions.toml
  • plugins/store/permissions/autogenerated/commands/get_subscription_status.toml
  • plugins/store/permissions/autogenerated/commands/get_products.toml
🚧 Files skipped from review as they are similar to previous changes (5)
  • frontend/src-tauri/capabilities/default.json
  • frontend/src-tauri/capabilities/mobile-ios.json
  • plugins/store/src/lib.rs
  • frontend/src/utils/region-gate.ts
  • plugins/store/permissions/schemas/schema.json
🧰 Additional context used
🪛 GitHub Actions: Desktop App CI
frontend/src/routes/pricing.tsx

[error] 2-2: TypeScript error TS6133: 'Suspense' is declared but its value is never read.


[error] 22-22: TypeScript error TS6133: 'transactionId' is declared but its value is never read.

frontend/src/components/ApplePayButton.tsx

[error] 1-1: TypeScript error TS6133: 'React' is declared but its value is never read.

🪛 GitHub Actions: Mobile App CI
frontend/src/routes/pricing.tsx

[error] 2-2: TypeScript error TS6133: 'Suspense' is declared but its value is never read.


[error] 22-22: TypeScript error TS6133: 'transactionId' is declared but its value is never read.

frontend/src/components/ApplePayButton.tsx

[error] 1-1: TypeScript error TS6133: 'React' is declared but its value is never read.

🪛 SwiftLint (0.57.0)
plugins/store/ios/Sources/StorePlugin.swift

[Warning] 21-21: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (53)
plugins/store/build.rs (2)

1-9: Commands align well with the plugin's purpose

The defined commands appropriately cover the functionality needed for store operations, including region detection which is crucial for the US-only external billing feature.


11-15: Clean and concise Tauri plugin setup

The plugin builder implementation is correct and follows Tauri plugin patterns with proper platform-specific configuration.

plugins/store/permissions/autogenerated/reference.md (1)

1-192: Well-structured permissions documentation

This reference document provides a comprehensive and consistent list of all store plugin permissions, which aligns perfectly with the commands defined in the build script. The table format makes it easy to understand the available permissions.

frontend/src/billing/billingApi.ts (1)

303-346: Good implementation of Apple transaction synchronization

The syncAppleTransaction function follows the established patterns in the file with proper error handling, logging, and authorization. The function correctly handles 401 responses and other error scenarios, which is essential for payment processing.

frontend/src/components/ApplePayButton.tsx (3)

7-15: Well-defined props interface

The props interface is clear and comprehensive, including all necessary props for the component's functionality.


48-96: Robust purchase flow implementation

The purchase flow is well-implemented with appropriate error handling and status checks. I especially appreciate:

  • Handling both success and pending states for purchases
  • The fallback for successful purchases even when backend sync fails
  • Clear distinction between cancellation and error cases

98-129: Clear and accessible button UI

The button implementation includes appropriate visual feedback for loading and disabled states, and the Apple logo adds clear branding.

frontend/src/billing/billingService.ts (5)

10-11: API imports updated to support Apple transactions.

The imports have been extended to include fetchTeamPlanAvailable and syncAppleTransaction, preparing the service for Apple in-app purchases integration.


13-13: Import for region-based gating added.

The allowExternalBilling utility is imported to support determining whether to show Apple Pay based on the user's App Store region.


24-26: Method visibility changed from private to public.

The getStoredToken method was previously private and has been made public to support token retrieval for Apple Pay operations. This change adheres to the principle of minimal exposure while still allowing necessary access.


100-111: New method for syncing Apple transactions with backend.

The syncAppleTransaction method correctly uses the token-based authentication pattern consistent with other API calls in this service.


117-138: Implemented region-based Apple Pay display logic.

The implementation correctly:

  1. Checks for iOS platform using dynamic import
  2. Determines if external billing is allowed based on region
  3. Returns the inverted result (show Apple Pay if external billing is NOT allowed)
  4. Handles errors gracefully with appropriate default behavior

This ensures Apple Pay is only shown when required by App Store guidelines.

plugins/store/src/commands.rs (8)

1-5: Imports and module references properly established.

The file correctly imports Tauri-specific types and traits needed for creating commands and handling results.


6-11: Region retrieval command correctly implemented.

The get_region command follows the Tauri command pattern, returning a Result<String> which is appropriate for this operation. The command delegates to the app's store instance, maintaining a clean separation of concerns.


13-19: Product fetching command correctly implemented.

The command properly handles a vector of product IDs and returns product details, maintaining type safety with the appropriate return type.


21-27: Purchase command implementation looks good.

The command accepts a product ID and returns a purchase result, maintaining the expected pattern for Tauri commands.


29-36: Verification command properly implemented.

The command accepts both product ID and transaction ID parameters required for verification, with appropriate return type.


38-44: Transaction retrieval command looks good.

The command correctly implements optional filtering by product ID, making it flexible for different use cases.


46-51: Restore purchases command properly implemented.

The command follows the expected pattern for Tauri commands with no parameters and returns the appropriate result type.


53-59: Subscription status command correctly implemented.

The command accepts a product ID parameter and returns the subscription status with the appropriate type.

frontend/src/billing/applePayService.ts (12)

1-3: Dependencies correctly imported.

The file imports Tauri's invoke functionality and the region-gating utility required for Apple Pay implementation.


5-80: Comprehensive type definitions for StoreKit integration.

The type definitions properly model Apple's StoreKit entities with appropriate properties and types. This ensures type safety when interacting with the native plugin.


92-106: Service implemented as singleton with appropriate initialization.

The singleton pattern is correctly implemented with a private constructor and static getInstance method.


111-113: Product ID map customization preserves defaults.

The implementation correctly merges custom mappings with defaults, ensuring core mappings are maintained while allowing for extension.


118-120: Fallback handling for unmapped product IDs.

The method returns the input ID if no mapping exists, providing graceful handling for unknown product IDs.


148-165: Apple Pay required check correctly implemented.

The method properly checks platform and region to determine if Apple Pay is required, using the allowExternalBilling utility to check for US regions.


170-187: Product fetching with caching properly implemented.

The method correctly fetches products via the Tauri plugin and caches them for later use, with appropriate error handling and logging.


192-202: Core Apple Pay operations implemented with consistent patterns.

The purchase, verifyPurchase, getTransactions, restorePurchases, and getSubscriptionStatus methods follow consistent patterns with:

  • Clear logging
  • Proper error handling
  • Consistent use of Tauri's invoke API
  • Appropriate parameter passing

This ensures consistent behavior across different store operations.

Also applies to: 204-218, 220-233, 235-246, 248-261


266-271: Price formatting uses browser locale.

The formatPrice method correctly uses the browser's locale for currency formatting, ensuring prices appear in the user's expected format.


276-278: Simple cache access method provided.

The getCachedProduct method provides a simple interface to access cached products without exposing the internal cache implementation.


281-282: Singleton instance exported as a constant.

The service is properly initialized and exported as a constant, making it easy to import and use throughout the application.


86-90:

Details

✅ Verification successful

Product ID mapping limited to monthly plans.

The current mapping only includes monthly plans for Apple Pay, which may be insufficient if users can select other billing periods.

Verify whether annual or other subscription periods need to be supported through Apple Pay. If quarterly or annual subscriptions should be available, additional product IDs will need to be added to the mapping.


🏁 Script executed:

#!/bin/bash
# Check for other subscription periods in the codebase
echo "Searching for other subscription periods in the code..."
rg "price_\w+_(annual|yearly|quarterly)" --type ts

Length of output: 173


🏁 Script executed:

#!/bin/bash
# Extract all Stripe product IDs ("price_*") in TypeScript files to verify available plans
echo "Extracting all Stripe product IDs in the codebase..."
rg -o --type ts "price_[A-Za-z0-9_]+" | sort -u

Length of output: 284


No additional subscription plans detected—mapping is complete
We searched the codebase for other Stripe product IDs (annual, quarterly, etc.) and only found price_pro_monthly and price_starter_monthly. The current PRODUCT_ID_MAP covers all existing plans; no further IDs need adding. If you introduce annual or other billing periods later, update this map accordingly.

plugins/store/ios/Sources/StorePlugin.swift (16)

6-8: Required frameworks imported.

The file correctly imports StoreKit for in-app purchases and the Tauri framework for plugin integration.


10-21: Plugin class with proper iOS version constraint and properties.

The StorePlugin class is correctly marked as available only on iOS 15.0 and later, which is required for StoreKit 2 support. The properties for tracking transactions and products are appropriately defined.

🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 21-21: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)


25-34: Proper initialization and cleanup in lifecycle methods.

The plugin correctly starts the transaction listener in init() and cancels it in deinit, ensuring proper resource management.


38-53: Transaction listener implementation.

The transaction listener is properly implemented as an asynchronous task that processes transactions as they arrive from StoreKit.


55-64: Verification result handling.

The helper method properly handles different verification states, returning the item while logging errors for unverified items, which is a balanced approach.


80-116: Region detection with robust fallback strategy.

The getRegion implementation properly:

  1. Tries StoreKit 2 API first
  2. Falls back to StoreKit 1 if necessary
  3. Attempts to use device locale as a final fallback
  4. Returns "UNKNOWN" only if all other methods fail

This provides a robust approach to region detection with multiple fallback strategies.


120-143: Product retrieval implementation.

The getProducts method correctly:

  1. Validates arguments
  2. Fetches products from StoreKit
  3. Stores them for later use
  4. Converts them to JSON-serializable format
  5. Handles errors properly

This provides a complete implementation for product retrieval.


147-211: Purchase implementation with comprehensive result handling.

The purchase method includes thorough error handling and properly manages different purchase states (success, cancelled, pending). It also includes verification and transaction finishing.


215-257: Purchase verification implementation.

The verifyPurchase method correctly:

  1. Validates arguments
  2. Searches for the relevant transaction
  3. Returns verification status with expiration information
  4. Handles errors properly

This provides a robust verification mechanism.


261-291: Transaction retrieval with filtering.

The getTransactions method properly fetches current entitlements and optionally filters by product ID, providing flexibility for different use cases.


295-319: Restore purchases implementation.

The method properly syncs with the App Store, fetches current entitlements, and returns them to the caller, with appropriate error handling.


323-377: Subscription status retrieval.

The getSubscriptionStatus method thoroughly checks for matching subscriptions and provides detailed status information, including auto-renewal and grace period details.


381-419: Helper methods for type conversions.

The helper methods properly convert StoreKit types to string representations for JSON serialization, with comprehensive handling of all possible states.


424-467: Product extension for JSON representation.

The extension correctly converts Product properties to a dictionary format suitable for JSON serialization, including special handling for subscription-specific fields.


469-492: Additional extensions for JSON representations.

Extensions for subscription periods, offers, transactions, and product types correctly convert StoreKit-specific types to JSON-compatible formats with comprehensive handling of all possible values.

Also applies to: 494-549, 551-567, 569-584


589-593: Plugin initialization export.

The C-callable function properly creates and returns an instance of the plugin for integration with the Tauri framework.

frontend/src/routes/pricing.tsx (2)

733-754: Good implementation of region-based notification.

This notification clearly explains to users why they can't access certain payment options based on their App Store region. The inclusion of the detected region provides helpful context.


868-883: Well-implemented conditional rendering for region-based billing options.

The code correctly adapts the UI to show Apple Pay buttons for iOS users in non-US regions, with appropriate messaging to indicate that in-app purchases are required. This provides a good user experience by offering alternative payment options where available.

plugins/store/src/mobile.rs (3)

8-27: Good platform-specific implementation with fallback.

The plugin initialization properly handles iOS-specific code with conditional compilation, providing a clean fallback for non-iOS platforms. This approach ensures that the code will compile and provide a consistent API across platforms.


33-52: Well-structured Transaction data model.

The Transaction struct is well-organized with appropriate field naming and serialization attributes. The use of serde rename attributes maintains a consistent API between snake_case Rust naming and camelCase JSON fields.


159-222: Consistent error handling pattern across methods.

The implementation of the Store struct methods follows a consistent pattern for calling plugin methods and handling errors. The use of .map_err(Into::into) for uniform error conversion is a good practice.

Consider adding more detailed error messages by extending the error conversion:

.map_err(|e| {
    crate::Error::Plugin(format!("Failed to get region: {}", e))
})

@@ -0,0 +1,130 @@
import React, { useState, useEffect } from "react";
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove unused React import

The import of React is not directly used in the code and is causing TypeScript errors in the pipeline.

Fix the import by removing React from the named imports:

-import React, { useState, useEffect } from "react";
+import { useState, useEffect } from "react";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
🧰 Tools
🪛 GitHub Actions: Desktop App CI

[error] 1-1: TypeScript error TS6133: 'React' is declared but its value is never read.

🪛 GitHub Actions: Mobile App CI

[error] 1-1: TypeScript error TS6133: 'React' is declared but its value is never read.

Comment on lines +125 to +143
public async isApplePayAvailable(): Promise<boolean> {
try {
// First check if we're on iOS
const { type } = await import("@tauri-apps/plugin-os");
const platform = await type();

if (platform !== "ios") {
console.log("[ApplePay] Not on iOS platform");
return false;
}

// For regions where external billing is not allowed, Apple Pay is required
// For US regions, we can show both options
return true;
} catch (error) {
console.error("[ApplePay] Error checking availability:", error);
return false;
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Incomplete availability check logic.

The isApplePayAvailable method only checks if the platform is iOS but doesn't consider region restrictions, making it potentially misleading.

The method returns true for all iOS devices regardless of region, which contradicts the region-based gating purpose of this PR. This could lead to Apple Pay being shown in regions where it should be prohibited.

- public async isApplePayAvailable(): Promise<boolean> {
-   try {
-     // First check if we're on iOS
-     const { type } = await import("@tauri-apps/plugin-os");
-     const platform = await type();
-     
-     if (platform !== "ios") {
-       console.log("[ApplePay] Not on iOS platform");
-       return false;
-     }
-     
-     // For regions where external billing is not allowed, Apple Pay is required
-     // For US regions, we can show both options
-     return true;
-   } catch (error) {
-     console.error("[ApplePay] Error checking availability:", error);
-     return false;
-   }
- }
+ public async isApplePayAvailable(): Promise<boolean> {
+   try {
+     // First check if we're on iOS
+     const { type } = await import("@tauri-apps/plugin-os");
+     const platform = await type();
+     
+     if (platform !== "ios") {
+       console.log("[ApplePay] Not on iOS platform");
+       return false;
+     }
+     
+     // Apple Pay is available on all iOS devices
+     return true;
+   } catch (error) {
+     console.error("[ApplePay] Error checking availability:", error);
+     return false;
+   }
+ }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +66 to +76
private func handleTransactionUpdate(_ transaction: Transaction) async {
// Notify your server about the transaction

// For consumables, manage them appropriately

// Finish the transaction to inform Apple the transaction is complete
await transaction.finish()

// Send a notification to the frontend if needed
emit("transactionUpdated", transaction.jsonRepresentation)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Transaction update handler.

The handler finishes transactions and emits an event to the frontend, but the commented sections suggest incomplete implementation.

The handleTransactionUpdate method has commented placeholders for server notification and consumable management that should be implemented:

private func handleTransactionUpdate(_ transaction: Transaction) async {
    // Notify your server about the transaction
+   // Implement server notification logic here
    
    // For consumables, manage them appropriately
+   // Add handling for consumable products here
    
    // Finish the transaction to inform Apple the transaction is complete
    await transaction.finish()
    
    // Send a notification to the frontend if needed
    emit("transactionUpdated", transaction.jsonRepresentation)
}

Committable suggestion skipped: line range outside the PR's diff.

@@ -1,17 +1,48 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useState, useEffect, useCallback } from "react";
import { useState, useEffect, useCallback, Suspense } from "react";
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix unused import of Suspense.

The Suspense import from React is not used anywhere in this file, which is causing a TypeScript error in the build pipeline.

-import { useState, useEffect, useCallback, Suspense } from "react";
+import { useState, useEffect, useCallback } from "react";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { useState, useEffect, useCallback, Suspense } from "react";
-import { useState, useEffect, useCallback, Suspense } from "react";
+import { useState, useEffect, useCallback } from "react";
🧰 Tools
🪛 GitHub Actions: Desktop App CI

[error] 2-2: TypeScript error TS6133: 'Suspense' is declared but its value is never read.

🪛 GitHub Actions: Mobile App CI

[error] 2-2: TypeScript error TS6133: 'Suspense' is declared but its value is never read.

Comment on lines +17 to +45
// Wrapper for Apple Pay button to handle success/error/cancel
function ApplePayButtonWrapper({ productId, className }: { productId: string, className?: string }) {
const queryClient = useQueryClient();
const [error, setError] = useState<string | null>(null);

const handleSuccess = async (transactionId: number) => {
try {
// Refresh billing status after successful purchase
await queryClient.invalidateQueries({ queryKey: ["billingStatus"] });
} catch (error) {
console.error("Error handling successful Apple Pay transaction:", error);
}
};

return (
<div className="flex flex-col">
<ApplePayButton
productId={productId}
className={className}
onSuccess={handleSuccess}
onError={(error) => setError(error.message)}
text="Subscribe with"
/>
{error && (
<div className="text-xs text-red-500 mt-1">{error}</div>
)}
</div>
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix unused parameter in handleSuccess function.

The transactionId parameter is declared but never used, triggering a TypeScript error in the build pipeline.

-  const handleSuccess = async (transactionId: number) => {
+  const handleSuccess = async (_transactionId: number) => {

The ApplePayButtonWrapper component provides a good abstraction for handling Apple Pay transactions, but the implementation should have proper error handling and success flow.

Consider enhancing user feedback for successful payments by adding a success message similar to how errors are displayed.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Wrapper for Apple Pay button to handle success/error/cancel
function ApplePayButtonWrapper({ productId, className }: { productId: string, className?: string }) {
const queryClient = useQueryClient();
const [error, setError] = useState<string | null>(null);
const handleSuccess = async (transactionId: number) => {
try {
// Refresh billing status after successful purchase
await queryClient.invalidateQueries({ queryKey: ["billingStatus"] });
} catch (error) {
console.error("Error handling successful Apple Pay transaction:", error);
}
};
return (
<div className="flex flex-col">
<ApplePayButton
productId={productId}
className={className}
onSuccess={handleSuccess}
onError={(error) => setError(error.message)}
text="Subscribe with"
/>
{error && (
<div className="text-xs text-red-500 mt-1">{error}</div>
)}
</div>
);
}
// Wrapper for Apple Pay button to handle success/error/cancel
function ApplePayButtonWrapper({ productId, className }: { productId: string, className?: string }) {
const queryClient = useQueryClient();
const [error, setError] = useState<string | null>(null);
const handleSuccess = async (_transactionId: number) => {
try {
// Refresh billing status after successful purchase
await queryClient.invalidateQueries({ queryKey: ["billingStatus"] });
} catch (error) {
console.error("Error handling successful Apple Pay transaction:", error);
}
};
return (
<div className="flex flex-col">
<ApplePayButton
productId={productId}
className={className}
onSuccess={handleSuccess}
onError={(error) => setError(error.message)}
text="Subscribe with"
/>
{error && (
<div className="text-xs text-red-500 mt-1">{error}</div>
)}
</div>
);
}
🧰 Tools
🪛 GitHub Actions: Desktop App CI

[error] 22-22: TypeScript error TS6133: 'transactionId' is declared but its value is never read.

🪛 GitHub Actions: Mobile App CI

[error] 22-22: TypeScript error TS6133: 'transactionId' is declared but its value is never read.

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.

2 participants