Skip to content

Commit a92ae33

Browse files
authored
feat(update): use App Store update card on iOS (#65)
1 parent 10c767f commit a92ae33

File tree

5 files changed

+150
-0
lines changed

5 files changed

+150
-0
lines changed

ios/Runner.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
AA1234561234567890ABCDE5 /* OpenListBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA1234561234567890ABCDE4 /* OpenListBridge.swift */; };
1717
AA1234561234567890ABCDE7 /* AppConfigBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA1234561234567890ABCDE6 /* AppConfigBridge.swift */; };
1818
AA1234561234567890ABCDE9 /* CommonBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA1234561234567890ABCDE8 /* CommonBridge.swift */; };
19+
AA1234561234567890ABCDEF /* AppStoreUpdateBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA1234561234567890ABCDEE /* AppStoreUpdateBridge.swift */; };
1920
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
2021
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
2122
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
@@ -57,6 +58,7 @@
5758
AA1234561234567890ABCDE4 /* OpenListBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenListBridge.swift; sourceTree = "<group>"; };
5859
AA1234561234567890ABCDE6 /* AppConfigBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfigBridge.swift; sourceTree = "<group>"; };
5960
AA1234561234567890ABCDE8 /* CommonBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonBridge.swift; sourceTree = "<group>"; };
61+
AA1234561234567890ABCDEE /* AppStoreUpdateBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreUpdateBridge.swift; sourceTree = "<group>"; };
6062
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
6163
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
6264
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
@@ -137,6 +139,7 @@
137139
AA1234561234567890ABCDEA /* Bridges */ = {
138140
isa = PBXGroup;
139141
children = (
142+
AA1234561234567890ABCDEE /* AppStoreUpdateBridge.swift */,
140143
AA1234561234567890ABCDE4 /* OpenListBridge.swift */,
141144
AA1234561234567890ABCDE6 /* AppConfigBridge.swift */,
142145
AA1234561234567890ABCDE8 /* CommonBridge.swift */,
@@ -295,6 +298,7 @@
295298
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
296299
AA1234561234567890ABCDE1 /* PigeonApi.swift in Sources */,
297300
AA1234561234567890ABCDE3 /* OpenListManager.swift in Sources */,
301+
AA1234561234567890ABCDEF /* AppStoreUpdateBridge.swift in Sources */,
298302
AA1234561234567890ABCDE5 /* OpenListBridge.swift in Sources */,
299303
AA1234561234567890ABCDE7 /* AppConfigBridge.swift in Sources */,
300304
AA1234561234567890ABCDE9 /* CommonBridge.swift in Sources */,

ios/Runner/AppDelegate.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import UIKit
55
@objc class AppDelegate: FlutterAppDelegate {
66
var eventAPI: Event?
77
private var backgroundTask: UIBackgroundTaskIdentifier = .invalid
8+
private var appStoreUpdateBridge: AppStoreUpdateBridge?
89

910
override func application(
1011
_ application: UIApplication,
@@ -24,6 +25,21 @@ import UIKit
2425
AppConfigSetup.setUp(binaryMessenger: messenger, api: AppConfigBridge())
2526
AndroidSetup.setUp(binaryMessenger: messenger, api: OpenListBridge())
2627
NativeCommonSetup.setUp(binaryMessenger: messenger, api: CommonBridge(viewController: controller))
28+
29+
let appStoreChannel = FlutterMethodChannel(
30+
name: "openlist/app_store_update",
31+
binaryMessenger: messenger
32+
)
33+
let appStoreBridge = AppStoreUpdateBridge(viewController: controller)
34+
appStoreUpdateBridge = appStoreBridge
35+
appStoreChannel.setMethodCallHandler { call, result in
36+
switch call.method {
37+
case "checkAndShowUpdate":
38+
appStoreBridge.checkAndShowUpdate(result: result)
39+
default:
40+
result(FlutterMethodNotImplemented)
41+
}
42+
}
2743

2844
// Setup Event API for Flutter callbacks
2945
eventAPI = Event(binaryMessenger: messenger)
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import Foundation
2+
import StoreKit
3+
import UIKit
4+
import Flutter
5+
6+
final class AppStoreUpdateBridge: NSObject, SKStoreProductViewControllerDelegate {
7+
private weak var viewController: UIViewController?
8+
9+
init(viewController: UIViewController?) {
10+
self.viewController = viewController
11+
super.init()
12+
}
13+
14+
func checkAndShowUpdate(result: @escaping FlutterResult) {
15+
guard let bundleId = Bundle.main.bundleIdentifier else {
16+
result(false)
17+
return
18+
}
19+
20+
let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0"
21+
let urlString = "https://itunes.apple.com/lookup?bundleId=\(bundleId)"
22+
guard let url = URL(string: urlString) else {
23+
result(false)
24+
return
25+
}
26+
27+
URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
28+
if let error = error {
29+
print("[AppStoreUpdateBridge] Apple API error: \(error)")
30+
result(false)
31+
return
32+
}
33+
34+
guard
35+
let data = data,
36+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
37+
let results = json["results"] as? [[String: Any]],
38+
let first = results.first,
39+
let latestVersion = first["version"] as? String,
40+
let trackId = first["trackId"] as? Int
41+
else {
42+
result(false)
43+
return
44+
}
45+
46+
let hasUpdate = self?.isVersionNewer(latest: latestVersion, current: currentVersion) ?? false
47+
if !hasUpdate {
48+
result(false)
49+
return
50+
}
51+
52+
self?.presentStoreProduct(trackId: trackId) { presented in
53+
result(presented)
54+
}
55+
}.resume()
56+
}
57+
58+
private func presentStoreProduct(trackId: Int, completion: @escaping (Bool) -> Void) {
59+
DispatchQueue.main.async { [weak self] in
60+
guard let presenter = self?.viewController ?? UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.rootViewController else {
61+
completion(false)
62+
return
63+
}
64+
65+
let storeVC = SKStoreProductViewController()
66+
storeVC.delegate = self
67+
let parameters = [SKStoreProductParameterITunesItemIdentifier: NSNumber(value: trackId)]
68+
storeVC.loadProduct(withParameters: parameters) { loaded, error in
69+
if let error = error {
70+
print("[AppStoreUpdateBridge] Failed to load product: \(error)")
71+
completion(false)
72+
return
73+
}
74+
if loaded {
75+
presenter.present(storeVC, animated: true) {
76+
completion(true)
77+
}
78+
} else {
79+
completion(false)
80+
}
81+
}
82+
}
83+
}
84+
85+
private func isVersionNewer(latest: String, current: String) -> Bool {
86+
let latestParts = latest.split(separator: ".").map { Int($0) ?? 0 }
87+
let currentParts = current.split(separator: ".").map { Int($0) ?? 0 }
88+
let count = max(latestParts.count, currentParts.count)
89+
90+
for index in 0..<count {
91+
let latestValue = index < latestParts.count ? latestParts[index] : 0
92+
let currentValue = index < currentParts.count ? currentParts[index] : 0
93+
if latestValue != currentValue {
94+
return latestValue > currentValue
95+
}
96+
}
97+
return false
98+
}
99+
100+
func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) {
101+
viewController.dismiss(animated: true)
102+
}
103+
}

lib/pages/app_update_dialog.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:flutter_smooth_markdown/flutter_smooth_markdown.dart';
44

55
import '../generated/l10n.dart';
66
import '../utils/update_checker.dart';
7+
import '../utils/app_store_update.dart';
78
import '../utils/intent_utils.dart';
89
import '../utils/download_manager.dart';
910

@@ -22,6 +23,16 @@ class AppUpdateDialog extends StatelessWidget {
2223

2324
static checkUpdateAndShowDialog(
2425
BuildContext context, ValueChanged<bool>? checkFinished) async {
26+
if (Platform.isIOS) {
27+
try {
28+
final hasNewVersion = await AppStoreUpdate.checkAndShowUpdate();
29+
checkFinished?.call(hasNewVersion);
30+
} catch (_) {
31+
checkFinished?.call(false);
32+
}
33+
return;
34+
}
35+
2536
final checker = UpdateChecker(owner: "openlistteam", repo: "OpenList-Mobile");
2637
await checker.downloadData();
2738
final hasNewVersion = await checker.hasNewVersion();

lib/utils/app_store_update.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import 'dart:io';
2+
3+
import 'package:flutter/services.dart';
4+
5+
class AppStoreUpdate {
6+
static const MethodChannel _channel = MethodChannel('openlist/app_store_update');
7+
8+
static Future<bool> checkAndShowUpdate() async {
9+
if (!Platform.isIOS) {
10+
return false;
11+
}
12+
13+
final result = await _channel.invokeMethod<bool>('checkAndShowUpdate');
14+
return result ?? false;
15+
}
16+
}

0 commit comments

Comments
 (0)