feat: iOS update check via iTunes Lookup API (checkUpdateIos) - #20
feat: iOS update check via iTunes Lookup API (checkUpdateIos)#20akshaynexus wants to merge 4 commits into
Conversation
|
there are some issues in teh pr will fix and comment here once fixed Edit: nvm the changes are fine ,just had to cross check with the api that the way im checking for updates is correct |
There was a problem hiding this comment.
Alternative approach: native Swift via method channel (no new packages)
The two new packages (package_info_plus, pub_semver) can be eliminated entirely by moving the implementation into the existing Swift plugin — matching the pattern already used by checkUpdateAndroid() on the Android side.
Everything these packages provide is already natively available in iOS:
| Need | Current approach | Native Swift equivalent |
|---|---|---|
| Bundle ID | PackageInfo.fromPlatform() |
Bundle.main.bundleIdentifier |
| Installed version | PackageInfo.fromPlatform() |
Bundle.main.infoDictionary?["CFBundleShortVersionString"] |
| Store region | developer-passed iosAppStoreRegion arg |
Locale.current.region?.identifier (iOS 16+) / Locale.current.regionCode (iOS < 16) |
| HTTP request | dart:io HttpClient |
URLSession.shared.dataTask(...) |
| JSON parsing | regex on raw string | JSONSerialization.jsonObject(...) |
| Version comparison | pub_semver Version.parse() |
v1.compare(v2, options: .numeric) |
Region handling
The iosAppStoreRegion parameter should be removed from the public API entirely. Asking the developer to pass a region has two failure modes:
- Developer hardcodes a region (e.g.
"in") — all users globally hit that store, getting wrong results - Developer omits it — iTunes Lookup API defaults to the US store, not geo-IP routing. App not listed in the US store silently returns
updateAvailable: falsefor all users
The correct source is Locale.current — this reads the 2-letter ISO alpha-2 region code (e.g. "us", "in", "gb") directly from the device language & region settings. No permission prompt, no Info.plist entries, no App Review concerns — it is the same API every app uses for localization. No lookup table or conversion needed.
Swift side
Add a checkUpdateIos case to InAppUpdateFlutterPlugin.handle():
case "checkUpdateIos":
let bundleId = Bundle.main.bundleIdentifier ?? ""
let installedVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
let regionCode: String
if #available(iOS 16, *) {
regionCode = Locale.current.region?.identifier.lowercased() ?? ""
} else {
regionCode = Locale.current.regionCode?.lowercased() ?? ""
}
let regionPath = regionCode.isEmpty ? "" : "/\(regionCode)"
let urlString = "https://itunes.apple.com\(regionPath)/lookup?bundleId=\(bundleId)"
guard let url = URL(string: urlString) else {
result(["storeVersion": "", "installedVersion": installedVersion, "updateAvailable": false, "bundleId": bundleId])
return
}
let request = URLRequest(url: url, timeoutInterval: 10)
URLSession.shared.dataTask(with: request) { data, _, _ in
guard let data = data,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let results = json["results"] as? [[String: Any]],
let storeVersion = results.first?["version"] as? String else {
DispatchQueue.main.async {
result(["storeVersion": "", "installedVersion": installedVersion, "updateAvailable": false, "bundleId": bundleId])
}
return
}
let updateAvailable = storeVersion.compare(installedVersion, options: .numeric) == .orderedDescending
DispatchQueue.main.async {
result(["storeVersion": storeVersion, "installedVersion": installedVersion, "updateAvailable": updateAvailable, "bundleId": bundleId])
}
}.resume()Dart side
No region parameter — just a method channel call, identical in shape to checkUpdateAndroid():
@override
Future<AppUpdateInfoIos> checkUpdateIos() async {
final result = await _methodChannel.invokeMapMethod<String, dynamic>("checkUpdateIos");
return AppUpdateInfoIos(
storeVersion: result?["storeVersion"] as String? ?? "",
installedVersion: result?["installedVersion"] as String? ?? "",
updateAvailable: result?["updateAvailable"] as bool? ?? false,
bundleId: result?["bundleId"] as String? ?? "",
);
}And the public API simplifies to:
Future<AppUpdateInfoIos> checkUpdateIos() {
return InAppUpdateFlutterPlatform.instance.checkUpdateIos();
}Additional benefits
Locale.currentreturns 2-letter alpha-2 directly — no lookup table, no conversion, not deprecatedDispatchQueue.main.asyncensuresFlutterResultis always called on the main thread — URLSession callbacks run on a background thread and callingresult()off the main thread causes undefined behavior.numericstring comparison handles non-semver App Store versions like1.0or2.3.4.5correctly —pub_semverthrowsFormatExceptionon theseURLSessiontimeout set natively (10 s) with no extra plumbing- JSON parsed via
JSONSerializationtargetingresults[0].version— not a regex that could match anywhere in the document - Dart method channel test pattern already established in
in_app_update_flutter_method_channel_test.dart
e5e2bba to
068a1aa
Compare
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughAdds ChangesiOS Update Check Feature
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
ios/in_app_update_flutter/Sources/in_app_update_flutter/InAppUpdateFlutterPlugin.swift (1)
57-58: 💤 Low valueConsider URL-encoding the bundle ID.
While bundle identifiers typically contain only URL-safe characters (alphanumerics, dots, hyphens), applying
addingPercentEncoding(withAllowedCharacters:)would be a defensive safeguard against unexpected characters.🛡️ Suggested defensive encoding
- guard !bundleId.isEmpty, - let url = URL(string: "https://itunes.apple.com\(regionPath)/lookup?bundleId=\(bundleId)") else { + guard !bundleId.isEmpty, + let encodedBundleId = bundleId.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), + let url = URL(string: "https://itunes.apple.com\(regionPath)/lookup?bundleId=\(encodedBundleId)") else {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ios/in_app_update_flutter/Sources/in_app_update_flutter/InAppUpdateFlutterPlugin.swift` around lines 57 - 58, The bundle ID is being directly interpolated into the URL string without URL encoding. Apply `addingPercentEncoding(withAllowedCharacters:)` to the bundleId variable before interpolating it into the URL string in the guard statement where URL(string:) is called. This ensures that any unexpected special characters in the bundle ID are properly escaped in the URL to prevent potential issues with the iTunes lookup request.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In
`@ios/in_app_update_flutter/Sources/in_app_update_flutter/InAppUpdateFlutterPlugin.swift`:
- Around line 57-58: The bundle ID is being directly interpolated into the URL
string without URL encoding. Apply
`addingPercentEncoding(withAllowedCharacters:)` to the bundleId variable before
interpolating it into the URL string in the guard statement where URL(string:)
is called. This ensures that any unexpected special characters in the bundle ID
are properly escaped in the URL to prevent potential issues with the iTunes
lookup request.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 22031edf-245d-4259-b15d-69aaa8ca2fff
⛔ Files ignored due to path filters (1)
example/pubspec.lockis excluded by!**/*.lock
📒 Files selected for processing (9)
README.mdios/in_app_update_flutter/Sources/in_app_update_flutter/InAppUpdateFlutterPlugin.swiftlib/in_app_update_flutter.dartlib/src/method_channel/in_app_update_flutter_method_channel.dartlib/src/models/app_update_info_ios.dartlib/src/models/models.dartlib/src/platform_interface/in_app_update_flutter_platform_interface.darttest/in_app_update_flutter_method_channel_test.darttest/in_app_update_flutter_test.dart
✅ Files skipped from review due to trivial changes (2)
- lib/src/models/models.dart
- README.md
068a1aa to
db2a506
Compare
Alternative approach: native Swift via method channel (no new packages)The two new packages ( Everything these packages provide is already natively available in iOS:
Region handlingThe
The correct source is Swift sideAdd a case "checkUpdateIos":
let bundleId = Bundle.main.bundleIdentifier ?? ""
let installedVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
let regionCode: String
if #available(iOS 16, *) {
regionCode = Locale.current.region?.identifier.lowercased() ?? ""
} else {
regionCode = Locale.current.regionCode?.lowercased() ?? ""
}
let regionPath = regionCode.isEmpty ? "" : "/\(regionCode)"
let urlString = "https://itunes.apple.com\(regionPath)/lookup?bundleId=\(bundleId)"
guard let url = URL(string: urlString) else {
result(["storeVersion": "", "installedVersion": installedVersion, "updateAvailable": false, "bundleId": bundleId])
return
}
let request = URLRequest(url: url, timeoutInterval: 10)
URLSession.shared.dataTask(with: request) { data, _, _ in
guard let data = data,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let results = json["results"] as? [[String: Any]],
let storeVersion = results.first?["version"] as? String else {
DispatchQueue.main.async {
result(["storeVersion": "", "installedVersion": installedVersion, "updateAvailable": false, "bundleId": bundleId])
}
return
}
let updateAvailable = storeVersion.compare(installedVersion, options: .numeric) == .orderedDescending
DispatchQueue.main.async {
result(["storeVersion": storeVersion, "installedVersion": installedVersion, "updateAvailable": updateAvailable, "bundleId": bundleId])
}
}.resume()Dart sideNo region parameter — just a method channel call, identical in shape to @override
Future<AppUpdateInfoIos> checkUpdateIos() async {
final result = await _methodChannel.invokeMapMethod<String, dynamic>("checkUpdateIos");
return AppUpdateInfoIos(
storeVersion: result?["storeVersion"] as String? ?? "",
installedVersion: result?["installedVersion"] as String? ?? "",
updateAvailable: result?["updateAvailable"] as bool? ?? false,
bundleId: result?["bundleId"] as String? ?? "",
);
}And the public API simplifies to: Future<AppUpdateInfoIos> checkUpdateIos() {
return InAppUpdateFlutterPlatform.instance.checkUpdateIos();
}Additional benefits
|
Adds checkUpdateIos() to check whether an App Store update is available before triggering any UI, mirroring the Android checkUpdateAndroid() pattern. Implemented natively in the existing Swift plugin via the method channel — no new Dart packages. - Add `checkUpdateIos` case to InAppUpdateFlutterPlugin: reads bundle id and installed version from Bundle.main, fetches the latest store version via URLSession + JSONSerialization, and compares with String.compare using .numeric (handles non-semver versions like "1.0" or "2.3.4.5") - Scope the lookup to the device's region (`Locale.current`), which is already an ISO alpha-2 code — the form the iTunes Lookup API expects, so no region parameter and no conversion table are needed. Without a region the API defaults to the US store, missing apps not listed there - Call FlutterResult on the main thread (URLSession callbacks run on a background queue) - Drop the `iosAppStoreRegion` parameter from the public API - Add AppUpdateInfoIos model (+ fromMap) and wire checkUpdateIos through the platform interface, method channel, and public API - Remove now-unneeded dependencies: package_info_plus, pub_semver - Add Dart method-channel and delegation tests; document in the README Closes axions-org#16 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
db2a506 to
39e16bb
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@ios/in_app_update_flutter/Sources/in_app_update_flutter/InAppUpdateFlutterPlugin.swift`:
- Line 35: The `installedVersion` is being defaulted to an empty string when the
bundle version cannot be retrieved, which allows `updateAvailable` to be set to
true even when the installed version is unavailable. At both locations where
`installedVersion` is extracted (the assignment at line 35 and again at line
74), add validation to ensure that `updateAvailable` is only set to true when
both the installed version and store version are available and non-empty. Treat
cases where the installed version is missing as non-comparable scenarios where
no update should be reported, rather than allowing empty string comparisons to
proceed.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: d9b550a7-0713-4af3-9e19-b42014322827
⛔ Files ignored due to path filters (1)
example/pubspec.lockis excluded by!**/*.lock
📒 Files selected for processing (9)
README.mdios/in_app_update_flutter/Sources/in_app_update_flutter/InAppUpdateFlutterPlugin.swiftlib/in_app_update_flutter.dartlib/src/method_channel/in_app_update_flutter_method_channel.dartlib/src/models/app_update_info_ios.dartlib/src/models/models.dartlib/src/platform_interface/in_app_update_flutter_platform_interface.darttest/in_app_update_flutter_method_channel_test.darttest/in_app_update_flutter_test.dart
✅ Files skipped from review due to trivial changes (2)
- lib/src/models/models.dart
- README.md
🚧 Files skipped from review as they are similar to previous changes (6)
- lib/src/platform_interface/in_app_update_flutter_platform_interface.dart
- test/in_app_update_flutter_method_channel_test.dart
- lib/src/method_channel/in_app_update_flutter_method_channel.dart
- lib/in_app_update_flutter.dart
- lib/src/models/app_update_info_ios.dart
- test/in_app_update_flutter_test.dart
- Treat an empty installedVersion as non-comparable so a missing bundle version can no longer report updateAvailable: true - URL-encode the bundle id before building the iTunes Lookup URL - Add a method-channel regression test asserting lookup failure returns updateAvailable: false (the non-throwing contract) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5a37a77769
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| regionCode = Locale.current.region?.identifier.lowercased() ?? "" | ||
| } else { | ||
| regionCode = Locale.current.regionCode?.lowercased() ?? "" |
There was a problem hiding this comment.
Use the storefront region for lookups
When the user's App Store account/storefront differs from the device Region setting (for example an expat or QA device set to US while the app is only listed in GB), this locale-based country builds the wrong iTunes Lookup URL. The lookup then either returns no result or a different storefront's version, so checkUpdateIos() reports updateAvailable: false even though the user can update from their actual App Store; use the StoreKit storefront or an explicit override instead of Locale.current.
Useful? React with 👍 / 👎.
Codex flagged that scoping the iTunes Lookup solely to Locale.current can target the wrong App Store when the device region differs from the user's storefront (e.g. an expat or QA device). SKStorefront.countryCode is the correct source but only exposes ISO 3166-1 alpha-3, with no clean Foundation alpha-3->alpha-2 conversion, so deriving it would require a hardcoded table. Instead, expose an optional `region` parameter on checkUpdateIos() (the explicit-override path Codex listed as acceptable). It takes an alpha-2 code and, when omitted, falls back to the device region as before — no lookup table. Native resolution is centralized in resolveRegion(override:). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes #16
Summary
Adds an iOS equivalent of the Android
checkUpdateAndroid()method, so callers can know whether an App Store update is available before triggering any UI.checkUpdateIos()queries the iTunes Lookup API for the latest published version and compares it against the installed version. It is implemented natively in the existing Swift plugin (over the method channel, mirroring the Android side) — no new Dart packages.Why native (vs. a pure-Dart implementation)
Everything needed is already available natively on iOS, so this avoids adding dependencies:
Bundle.main.bundleIdentifierBundle.main.infoDictionary["CFBundleShortVersionString"]Locale.current.region?.identifier(iOS 16+) /regionCodeURLSession(10s timeout)JSONSerialization(targetsresults[0].version)String.compare(_:options: .numeric)Region handling
The lookup is scoped to the device's region setting via
Locale.current, which already returns an ISO 3166-1 alpha-2 code (e.g.us,gb,in) — exactly what the iTunes Lookup API expects, so no lookup table or conversion is needed. This fixes two failure modes of a developer-passed region (hardcoding the wrong store, or defaulting to the US store for apps not listed there), so theiosAppStoreRegionparameter is removed from the public API. No permission prompt, noInfo.plistentries — it's the same API every app uses for localization.Changes
checkUpdateIoscase toInAppUpdateFlutterPlugin(nativeURLSession+JSONSerialization,.numericversion compare, device region viaLocale.current,FlutterResultdispatched on the main thread)AppUpdateInfoIosmodel (storeVersion,installedVersion,updateAvailable,bundleId) with afromMapfactorycheckUpdateIos()through the existing platform interface, method-channel implementation, and public API (platform interface kept intact)iosAppStoreRegionparameter from the public APIpackage_info_plus,pub_semvercheckUpdateIosin the README iOS Usage sectionNotes
.numericstring comparison handles non-semver App Store versions like1.0or2.3.4.5(whichpub_semverwould throw on).updateAvailable: falsewith an emptystoreVersionrather than throwing — see the open question in feat: iOS update check via iTunes Lookup API (checkUpdateIos) #16 if a different fallback is preferred.Test status
32/32Dart tests passing ·flutter analyzeclean ·dart formatclean · example iOS app builds (flutter build ios --simulator)🤖 Generated with Claude Code
Summary by CodeRabbit
checkUpdateIos()to perform iOS update checks and return store/installed version, availability, and bundle identifier.