-
Notifications
You must be signed in to change notification settings - Fork 5
Only consider US for apple external billing #62
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
|
""" WalkthroughThis 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
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
Possibly related PRs
Poem
Tip ⚡️ Faster reviews with caching
Enjoy the performance boost—your workflow just got faster. ✨ Finishing Touches
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed 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)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this 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.swiftlacks proper error handling for iOS versions below 13.0 allowExternalBilling()inregion-gate.tscould benefit from more granular error types instead of defaulting all errors to falsemobile.rsplugin 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
ca8f6f4 to
a5decc9
Compare
There was a problem hiding this 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 importYou're importing
UIKitwhen it appears this logger only usesos_log. Consider removing the unnecessary import to reduce dependencies.import os.log -import UIKit
24-37: String concatenation could be optimizedThe current approach to joining variadic parameters is inefficient. Consider using
Array.joined()orString.StringInterpolationfor 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 convertingDatatoString(optional_data_string_conversion)
plugins/store/.tauri/tauri-api/Sources/Tauri/Plugin/Plugin.swift (1)
27-31: Avoid force unwrapping inparseConfigmethod.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: AddadditionalProperties: falseat the top level to avoid silent schema driftWithout 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: Preferwhereclause instead ofifinside aforSwift-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:
whereclauses are preferred over a singleifinside afor(for_where)
plugins/store/.tauri/tauri-api/Sources/Tauri/JSTypes.swift (3)
24-61: Consider consistent type conversion handling across getter methods.The
getFloatmethod 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
dateStringFormatteris 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
⛔ Files ignored due to path filters (2)
frontend/src-tauri/Cargo.lockis excluded by!**/*.lockplugins/store/Cargo.lockis 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
StoreExttrait.plugins/store/.tauri/tauri-api/Sources/Tauri/Channel.swift (2)
44-50: LGTM! Good error handling in the serialization method.The
serializemethod 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 convertingDatatoString(optional_data_string_conversion)
plugins/store/src/mobile.rs (3)
31-37: LGTM! Clean implementation of the region fetching functionality.The
get_regionmethod 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 thePluginHandle<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 rustLength of output: 47
I couldn’t locate the
PluginHandledefinition 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 rustLength of output: 279
Let’s locate the
PluginHandledefinition in theplugincrate 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() }forPluginHandle<R>, as zero-initializing a type with non-trivial or reference fields can cause undefined behavior. I wasn’t able to locate thePluginHandledefinition—please manually verify that it contains only zero-compatible primitives (or implement a safe constructor/Defaultfor a dummy handle) before relying onzeroed().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:loadedflag never updated whenwebviewis nil – potential double-loadIf
loadis called before the webview exists,loadedremainsfalse; once the webview is later supplied,onWebviewCreatedwill load it again, executingloadtwice.
Either setloaded = trueafter any successfulsetConfig, or move the flag insidePlugin.load.plugins/store/.tauri/tauri-api/Sources/Tauri/Invoke.swift (1)
92-100: Dictionary merge inrejectallocates and force-casts – replace with safe merging
payload as! JsonObjectwill crash if any value fails to coerce toJsonValue. Build aJsonObjectdirectly 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
JSValueprotocol 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
JSObjectandJSArraytype 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 underplugins/store/.tauri/tauri-api/Sourcesuse bothApache-2.0andMITSPDX 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 patternsThis implementation correctly exposes the
get_regionfunctionality 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 variantsThe 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-specificPluginInvokeerror variant.
16-23: Implement serde::Serialize correctly for error serializationThe 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 licensesThe 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 scopedThe
JsonObjecttypealias andJsonValueenum with its nestedSerializationErrorare well-defined with appropriate visibility modifiers.
16-29: JSON serialization logic handles additional fields correctlyThe 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 thoroughThe 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.
a5decc9 to
ae440c0
Compare
There was a problem hiding this 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 issueAvoid 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 suggestionUse the failable
String(data:encoding:)initializer inresolve<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 convertingDatatoString(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 issueAvoid 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 issueAddress 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 issueReplace 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: Usefor whereclause for more readable code.When iterating and checking a condition, a
for whereclause is more readable than a separateifstatement 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:
whereclauses are preferred over a singleifinside afor(for_where)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
frontend/src-tauri/Cargo.lockis excluded by!**/*.lockplugins/store/Cargo.lockis 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.
ae440c0 to
a023139
Compare
There was a problem hiding this 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 suggestionAvoid lossy
String(decoding:as:)– switch to the failable initialiser
UsingString(decoding:as:)may silently replace invalid UTF-8 sequences, potentially corrupting the JSON forwarded to JavaScript. Prefer the failableString(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 convertingDatatoString(optional_data_string_conversion)
frontend/src/utils/region-gate.ts (1)
3-3: Model region codes with a TSenumor union type
Hard-coding region literals in an array makes accidental typos hard to spot and limits editor auto-completion. Defining anenum/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 issueCrash-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 issueForce-unwrap in
parseArgs<T>()mirrors the same crash risk
Apply the same guarded pattern as suggested forgetArgs()to prevent runtime crashes on invalid UTF-8.
68-72: 🛠️ Refactor suggestionUse safe UTF-8 conversion when encoding responses
As flagged by SwiftLint, prefer the failableString(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 convertingDatatoString(optional_data_string_conversion)
100-100:⚠️ Potential issueForce-cast
payload as! JsonObjectcan crash on non-dictionary typesFailure 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 issueFix 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 issueReplace force-cast with safe downcasting
The force-cast will crash the app if an object that doesn't conform to the
Pluginprotocol 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: Usefor-whereclause for cleaner codeReplace 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:
whereclauses are preferred over a singleifinside afor(for_where)
124-133: Remove redundant void return type declarationsThe
-> Voidreturn 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 productionConsole 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
⛔ Files ignored due to path filters (2)
frontend/src-tauri/Cargo.lockis excluded by!**/*.lockplugins/store/Cargo.lockis 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:
- Redundant optional initialization
- Unsafe type casting using
unsafeBitCast- 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
unsafeBitCastfor method invocation is brittle. Consider exploring safer alternatives like using@objcprotocols 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)
- Simplify optional initialization:
- var error: NSError? = nil + var error: NSError?- Replace brittle
unsafeBitCastdispatch:
- Consider using
perform(_:with:with:)or defining an@objcprotocol so you can call a typed method instead of casting theIMP.- 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 feedbackThe 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 implementationThe approach to detecting the region and gating external billing is well-implemented:
- Proper state initialization with safe defaults
- Dynamic importing of region-gating utilities
- Error handling that defaults to a secure state (blocking external billing on error)
- 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 UIThe 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 disablingThe 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.
| 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; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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; | |
| } |
a023139 to
06fb8a9
Compare
There was a problem hiding this 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 issueAvoid 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 issueAvoid 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 issueAvoid lossy
String(decoding:as:)conversionPrefer 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 convertingDatatoString(optional_data_string_conversion)
99-100:⚠️ Potential issueAvoid force casting in
reject()methodForce casting
payload as! JsonObjectcould 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 initializationForce 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 blockThe dynamic invocation implementation contains several issues:
- Redundant optional initialization (
var error: NSError? = nil)- Force-unwrapping of the method IMP
- 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 pluginsForce-casting to
Pluginwill 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: Usewhereclause instead of nestedifin for loopThe for loop can be simplified using a
whereclause 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:
whereclauses are preferred over a singleifinside afor(for_where)
124-133: Remove redundantVoidreturn type in closuresThe explicit return type of
Voidin 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
⛔ Files ignored due to path filters (2)
frontend/src-tauri/Cargo.lockis excluded by!**/*.lockplugins/store/Cargo.lockis 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 mechanismThe implementation of
getRegionproperly 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 levelsThe 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 initializationThe C-exported function
initPluginprovides 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 compatibilityThe
JSValueprotocol 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 methodsThe 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 conversionThe
coerceToJSValuefunction provides comprehensive type conversion with good handling of various Swift and Foundation types. The recursive processing of arrays and dictionaries ensures deep conversion.
| let json = try JSONEncoder().encode(data) | ||
| handler(id, String(decoding: json, as: UTF8.self)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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)
77cacfc to
89f84b7
Compare
There was a problem hiding this 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 codesUsing 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 issueFix 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 failableString(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 convertingDatatoString(optional_data_string_conversion)
plugins/store/.tauri/tauri-api/Sources/Tauri/Invoke.swift (4)
34-38:⚠️ Potential issueRemove force-cast when coercing JSON to
JSObject
as! NSDictionarywill crash on malformed payloads. Use optional binding and surface an error throughreject.- 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 issueAvoid 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 issueAvoid lossy
String(decoding:as:)conversionPrefer 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 convertingDatatoString(optional_data_string_conversion)
99-100:⚠️ Potential issueAvoid force casting in
reject()method.Force casting
payload as! JsonObjectcould 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 issueAvoid 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 issueMultiple issues in dynamic invocation block
- Redundant optional initialization (
var error: NSError? = nil) – just declarevar error: NSError?.- Force-casting the method IMP signature is brittle; consider moving to
@objcprotocol &performSelectorwith typed wrappers where feasible.- 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 issueReplace force-cast with safe down-cast when registering plugins
If a non-conforming
NSObjectis 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 limitThe 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 explicitlyThe 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 logicConsider 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: Usefor-whereclause for better readability and efficiencyReplace the
ifstatement inside theforloop with awhereclause 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:
whereclauses are preferred over a singleifinside afor(for_where)
126-129: Remove redundantVoidreturn type annotationsThe
-> Voidannotations 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
⛔ Files ignored due to path filters (2)
frontend/src-tauri/Cargo.lockis excluded by!**/*.lockplugins/store/Cargo.lockis 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
| export function isUSRegion(regionCode: string): boolean { | ||
| return US_CODES.includes(regionCode); | ||
| } |
There was a problem hiding this comment.
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.
| export function isUSRegion(regionCode: string): boolean { | |
| return US_CODES.includes(regionCode); | |
| } | |
| export function isUSRegion(regionCode: string): boolean { | |
| return US_CODES.includes(regionCode.toUpperCase()); | |
| } |
…d adding proper availability annotations
89f84b7 to
ef01fa5
Compare
There was a problem hiding this 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 suggestionAdd 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 loadingThe 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 ofshouldShowApplePay(), 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
transactionListenerTaskvariable 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
Productstruct 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
Defaulttrait 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_regionmethod 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
Storestruct 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
📒 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 purposeThe 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 setupThe 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 documentationThis 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 synchronizationThe
syncAppleTransactionfunction 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 interfaceThe props interface is clear and comprehensive, including all necessary props for the component's functionality.
48-96: Robust purchase flow implementationThe 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 UIThe 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
fetchTeamPlanAvailableandsyncAppleTransaction, preparing the service for Apple in-app purchases integration.
13-13: Import for region-based gating added.The
allowExternalBillingutility 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
getStoredTokenmethod 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
syncAppleTransactionmethod 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:
- Checks for iOS platform using dynamic import
- Determines if external billing is allowed based on region
- Returns the inverted result (show Apple Pay if external billing is NOT allowed)
- 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_regioncommand follows the Tauri command pattern, returning aResult<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
allowExternalBillingutility 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, andgetSubscriptionStatusmethods 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
formatPricemethod 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
getCachedProductmethod 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 tsLength 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 -uLength 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 foundprice_pro_monthlyandprice_starter_monthly. The currentPRODUCT_ID_MAPcovers 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
StorePluginclass 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 indeinit, 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
getRegionimplementation properly:
- Tries StoreKit 2 API first
- Falls back to StoreKit 1 if necessary
- Attempts to use device locale as a final fallback
- 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
getProductsmethod correctly:
- Validates arguments
- Fetches products from StoreKit
- Stores them for later use
- Converts them to JSON-serializable format
- Handles errors properly
This provides a complete implementation for product retrieval.
147-211: Purchase implementation with comprehensive result handling.The
purchasemethod 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
verifyPurchasemethod correctly:
- Validates arguments
- Searches for the relevant transaction
- Returns verification status with expiration information
- Handles errors properly
This provides a robust verification mechanism.
261-291: Transaction retrieval with filtering.The
getTransactionsmethod 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
getSubscriptionStatusmethod 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
Transactionstruct is well-organized with appropriate field naming and serialization attributes. The use ofserderename 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
Storestruct 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"; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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) | ||
| } |
There was a problem hiding this comment.
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"; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| // 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> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| // 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.
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Chores
.gitignorepatterns for Rust and iOS build artifacts.