Skip to content

Commit 2a873bb

Browse files
authored
[MINI-6258]Download Miniapp (#525)
* Download Miniapp * SDK Changes * Demo app changes * Sample app changes * TC fix * Changelog
1 parent d5440fe commit 2a873bb

File tree

11 files changed

+200
-19
lines changed

11 files changed

+200
-19
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
## CHANGELOG
22

3+
### 5.4.0 (2023-08-29)
4+
**SDK**
5+
- **Feature:** Added a new interface `downloadMiniApp(appId:versionId:completionHandler:)` to download Miniapp from platform in background if needed.
6+
- **Feature:** Added a utility method to know if Miniapp has beend downloaded properly. `isMiniAppCacheAvailable(appId: String, versionId:)`
7+
- **Refactor:** Updated unzip Miniapp method with optional completion handler that will let the host app know if the download is success. `unzipMiniApp(fileName:miniAppId:versionId:completionHandler:)`
8+
9+
**Sample App**
10+
- **Feature:** Added Menu in list view to Download and check if Miniapp is downloaded already.
11+
312
### 5.3.1 (2023-08-02)
413
**SDK**
514
- **Feature:** Updated `loadFromBundle(miniAppManifest:completionHandler)` interface with optional MiniAppManifest object.

Example/Controllers/SwiftUI/Features/List/MiniAppListRowCell.swift

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import SwiftUI
2+
import MiniApp
23

34
struct MiniAppListRowCell: View {
45

56
@State var iconUrl: URL?
67
@State var displayName: String
8+
@State var miniAppId: String
79
@State var versionTag: String
810
@State var versionId: String
11+
@State var listType: ListType
12+
@State private var showingAlert = false
13+
@State private var alertDescription = "false"
914

1015
var body: some View {
1116
HStack {
@@ -17,6 +22,9 @@ struct MiniAppListRowCell: View {
1722
.resizable()
1823
.aspectRatio(contentMode: .fill)
1924
.frame(width: 40, height: 40, alignment: .center)
25+
.contextMenu {
26+
menuItems
27+
}
2028
}, placeholder: {
2129
Rectangle()
2230
.fill(Color(.systemGray3))
@@ -25,6 +33,9 @@ struct MiniAppListRowCell: View {
2533
} else {
2634
RemoteImageView(urlString: iconUrl?.absoluteString ?? "")
2735
.frame(width: 60, height: 40, alignment: .center)
36+
.contextMenu {
37+
menuItems
38+
}
2839
}
2940
Spacer()
3041
}
@@ -50,17 +61,53 @@ struct MiniAppListRowCell: View {
5061
Spacer()
5162
}
5263
}
64+
.alert(isPresented: $showingAlert) {
65+
Alert(
66+
title: Text("Info"),
67+
message: Text(alertDescription),
68+
dismissButton: .default(Text("OK")))
69+
}
5370
.padding(10)
5471
}
5572
}
73+
74+
var menuItems: some View {
75+
Group {
76+
Button("Download", action: downloadMiniAppInBackground)
77+
Button("Available already?", action: isMiniAppCacheAvailable)
78+
}
79+
}
80+
81+
func downloadMiniAppInBackground() {
82+
MiniApp.shared(with: ListConfiguration(listType: listType).sdkConfig).downloadMiniApp(appId: miniAppId, versionId: versionId) { result in
83+
switch result {
84+
case .success:
85+
print("Download Completed")
86+
case .failure(let error):
87+
print("Error downloading Miniapp:", error)
88+
}
89+
}
90+
}
91+
92+
func isMiniAppCacheAvailable() {
93+
if MiniApp.isMiniAppCacheAvailable(appId: miniAppId, versionId: versionId) {
94+
showingAlert = true
95+
alertDescription = "MiniApp is available"
96+
} else {
97+
showingAlert = true
98+
alertDescription = "MiniApp is not downloaded"
99+
}
100+
}
56101
}
57102

58103
struct MiniAppListRowCell_Previews: PreviewProvider {
59104
static var previews: some View {
60105
MiniAppListRowCell(
61106
displayName: "MiniApp Sample",
107+
miniAppId: "123",
62108
versionTag: "0.7.2",
63-
versionId: "abcdefgh-12345678-abcdefgh-12345678"
109+
versionId: "abcdefgh-12345678-abcdefgh-12345678",
110+
listType: .listI
64111
)
65112
}
66113
}

Example/Controllers/SwiftUI/Features/List/MiniAppListView.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,10 @@ extension MiniAppListView {
113113
MiniAppListRowCell(
114114
iconUrl: info.icon,
115115
displayName: info.displayName ?? "",
116+
miniAppId: info.id,
116117
versionTag: info.version.versionTag,
117-
versionId: info.version.versionId
118+
versionId: info.version.versionId,
119+
listType: viewModel.type
118120
)
119121
}
120122
}

Sources/Classes/core/Downloader/MiniAppDownloader.swift

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class MiniAppDownloader: MiniAppDownloaderInterface {
5252
}
5353
}
5454

55-
private func download(appId: String, versionId: String, completionHandler: @escaping (Result<URL, MASDKError>) -> Void) {
55+
internal func download(appId: String, versionId: String, completionHandler: @escaping (Result<URL, MASDKError>) -> Void) {
5656
let miniAppStoragePath = FileManager.getMiniAppVersionDirectory(with: appId, and: versionId)
5757
self.manifestDownloader.fetchManifest(apiClient: self.miniAppClient, appId: appId, versionId: versionId) { (result) in
5858
switch result {
@@ -78,6 +78,28 @@ class MiniAppDownloader: MiniAppDownloaderInterface {
7878
}
7979
}
8080

81+
internal func downloadMiniApp(appId: String, versionId: String, completionHandler: @escaping (Result<URL, MASDKError>) -> Void) {
82+
let miniAppStoragePath = FileManager.getMiniAppVersionDirectory(with: appId, and: versionId)
83+
self.manifestDownloader.fetchManifest(apiClient: self.miniAppClient, appId: appId, versionId: versionId) { (result) in
84+
switch result {
85+
case .success(let responseData):
86+
self.startDownloadingFiles(urls: responseData.manifest, to: miniAppStoragePath, miniAppId: appId, miniAppVersion: versionId) { downloadResult in
87+
switch downloadResult {
88+
case .success:
89+
DispatchQueue.main.async {
90+
self.cacheVerifier.storeHash(for: appId, version: versionId)
91+
}
92+
fallthrough
93+
default:
94+
completionHandler(downloadResult)
95+
}
96+
}
97+
case .failure(let error):
98+
completionHandler(.failure(error))
99+
}
100+
}
101+
}
102+
81103
func isMiniAppAlreadyDownloaded(appId: String, versionId: String) -> Bool {
82104
if miniAppStatus.isDownloaded(appId: appId, versionId: versionId) {
83105
let versionDirectory = FileManager.getMiniAppVersionDirectory(with: appId, and: versionId)

Sources/Classes/core/MiniApp.swift

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,33 @@ public class MiniApp: NSObject {
206206
try? MiniAppSecureStorage.wipeSecureStorage(for: miniAppId)
207207
}
208208

209-
public static func unzipMiniApp(fileName: String, miniAppId: String, versionId: String) {
210-
MiniAppSDKUtility.unzipMiniApp(fileName: fileName, miniAppId: miniAppId, versionId: versionId)
209+
/// Unzip the provided archive file from bundle.
210+
/// - Parameters:
211+
/// - fileName: File name of the Bundle that you want to extract in MiniApp folder
212+
/// - miniAppId: MiniApp ID
213+
/// - versionId: Version ID
214+
/// - completionHandler: Status of the Unzip feature
215+
public static func unzipMiniApp(fileName: String, miniAppId: String, versionId: String, completionHandler: ((Result<Bool, MASDKError>) -> Void)? = nil) {
216+
MiniAppSDKUtility.unzipMiniApp(fileName: fileName, miniAppId: miniAppId, versionId: versionId, completionHandler: completionHandler)
217+
}
218+
219+
/// Download a specific MiniApp from the Platform, please note that the MiniApp should be available before start downloading
220+
/// - Parameters:
221+
/// - appId: MiniApp ID
222+
/// - versionId: VersionID of the MiniApp
223+
/// - completionHandler: Completion handler that tells the status of the download
224+
public func downloadMiniApp(appId: String, versionId: String, completionHandler: @escaping (Result<Bool, MASDKError>) -> Void) {
225+
realMiniApp.downloadMiniApp(appId: appId, versionId: versionId, completionHandler: completionHandler)
226+
}
227+
228+
/// Check and return TRUE if the MiniApp is available for a given MiniAppID and VersionID.
229+
/// This method not only checks if the folder is available, but also checks if index.html is available.
230+
/// - Parameters:
231+
/// - appId: MiniApp ID
232+
/// - versionId: VersionID of the MiniApp
233+
/// - Returns: TRUE if MiniApp is available
234+
public static func isMiniAppCacheAvailable(appId: String, versionId: String) -> Bool {
235+
return MiniAppSDKUtility.isMiniAppAvailable(appId: appId, versionId: versionId)
211236
}
212237
}
213238

Sources/Classes/core/RealMiniApp.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,23 @@ internal class RealMiniApp {
437437
completionHandler(.failure(.miniAppCorrupted))
438438
}
439439
}
440+
441+
func downloadMiniApp(appId: String, versionId: String, completionHandler: @escaping (Result<Bool, MASDKError>) -> Void) {
442+
if appId.isEmpty {
443+
return completionHandler(.failure(.invalidAppId))
444+
}
445+
if versionId.isEmpty {
446+
return completionHandler(.failure(.invalidVersionId))
447+
}
448+
miniAppDownloader.downloadMiniApp(appId: appId, versionId: versionId) { result in
449+
switch result {
450+
case .success:
451+
completionHandler(.success(true))
452+
case .failure(let error):
453+
completionHandler(.failure(error))
454+
}
455+
}
456+
}
440457
}
441458

442459
extension RealMiniApp: MiniAppMessageDelegate {

Sources/Classes/core/Utilities/MiniAppSDKUtility.swift

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,39 @@ import Foundation
22
import CommonCrypto
33

44
class MiniAppSDKUtility {
5-
internal static func unzipMiniApp(fileName: String, miniAppId: String, versionId: String) {
5+
internal static func unzipMiniApp(fileName: String, miniAppId: String, versionId: String, completionHandler: ((Result<Bool, MASDKError>) -> Void)? = nil) {
66
let cacheVerifier = MiniAppCacheVerifier()
77
guard let filePath = Bundle.main.url(forResource: fileName, withExtension: "zip") else {
8+
completionHandler?(.failure(.unknownError(domain: MASDKLocale.localize(.unknownError), code: 1, description: "MiniApp Bundle Zip file not found")))
89
return
910
}
1011
do {
1112
try FileManager.default.unzipItem(at: filePath, to: FileManager.getMiniAppVersionDirectory(with: miniAppId, and: versionId), skipCRC32: true)
1213
cacheVerifier.storeHash(for: miniAppId, version: versionId)
14+
return
1315
} catch let err {
14-
MiniAppLogger.e("error unzipping archive", err)
16+
MiniAppLogger.e("Error unzipping archive", err)
17+
completionHandler?(.failure(.unknownError(domain: MASDKLocale.localize(.unknownError), code: 1, description: "Failed to Unzip Miniapp with Error: \(err)")))
18+
return
19+
}
20+
}
21+
22+
internal static func cleanMiniAppVersions(appId: String, exceptForVersionId: String) {
23+
guard !appId.isEmpty, !exceptForVersionId.isEmpty else {
24+
return
25+
}
26+
MiniAppStorage.cleanVersions(for: appId, differentFrom: exceptForVersionId)
27+
}
28+
29+
internal static func isMiniAppAvailable(appId: String, versionId: String) -> Bool {
30+
guard !appId.isEmpty, !versionId.isEmpty else {
31+
return false
32+
}
33+
let versionDirectory = FileManager.getMiniAppVersionDirectory(with: appId, and: versionId)
34+
let miniAppRootPath = "\(versionDirectory.path)/\(Constants.rootFileName)"
35+
if FileManager.default.fileExists(atPath: miniAppRootPath) {
36+
return true
1537
}
38+
return false
1639
}
1740
}

Sources/Classes/core/View/MiniAppView.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ public class MiniAppView: UIView, MiniAppViewable {
186186
case let .success(webView):
187187
self?.state.send(.active)
188188
self?.setupWebView(webView: webView)
189+
self?.cleanVersions()
189190
completion(.success(true))
190191
case let .failure(error):
191192
self?.state.send(.error(error))
@@ -194,6 +195,16 @@ public class MiniAppView: UIView, MiniAppViewable {
194195
}
195196
}
196197

198+
/// This method will clean all versions of MiniApp except for the version that is passed/loaded
199+
internal func cleanVersions() {
200+
guard let versionId = miniAppHandler.version, !versionId.isEmpty else {
201+
return
202+
}
203+
if !miniAppHandler.appId.isEmpty {
204+
MiniAppSDKUtility.cleanMiniAppVersions(appId: miniAppHandler.appId, exceptForVersionId: versionId)
205+
}
206+
}
207+
197208
public var alertInfo: CloseAlertInfo? {
198209
return miniAppHandler.miniAppShouldClose()
199210
}

Sources/Classes/core/View/MiniAppViewHandler.swift

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ class MiniAppViewHandler: NSObject {
182182
queryParams: self.queryParams
183183
)
184184
} catch {
185-
completion(.failure(.unknownError(domain: "", code: 0, description: "internal error: could not load the miniapp")))
185+
completion(.failure(.unknownError(domain: "", code: 0, description: "Internal error: could not load the miniapp")))
186186
}
187187
completion(.success(newWebView))
188188
}
@@ -191,7 +191,7 @@ class MiniAppViewHandler: NSObject {
191191

192192
getMiniAppInfo(miniAppId: appId, miniAppVersion: version ?? "") { [weak self] result in
193193
guard let self = self else {
194-
completion(.failure(.unknownError(domain: "", code: 0, description: "miniapp download failed")))
194+
completion(.failure(.unknownError(domain: "", code: 0, description: "Miniapp download failed")))
195195
return
196196
}
197197
switch result {
@@ -202,7 +202,7 @@ class MiniAppViewHandler: NSObject {
202202
switch result {
203203
case let .success(state):
204204
guard state else {
205-
completion(.failure(.unknownError(domain: "", code: 0, description: "miniapp download failed")))
205+
completion(.failure(.unknownError(domain: "", code: 0, description: "MiniApp download failed")))
206206
return
207207
}
208208
MiniAppLogger.d("MiniApp loaded with state: \(state)")
@@ -222,7 +222,7 @@ class MiniAppViewHandler: NSObject {
222222
queryParams: self.queryParams
223223
)
224224
} catch {
225-
completion(.failure(.unknownError(domain: "", code: 0, description: "internal error")))
225+
completion(.failure(.unknownError(domain: "", code: 0, description: "Internal error")))
226226
}
227227
completion(.success(newWebView))
228228
}
@@ -240,10 +240,8 @@ class MiniAppViewHandler: NSObject {
240240
if appId.isEmpty {
241241
return completion(.failure(.invalidAppId))
242242
}
243-
guard
244-
!miniAppClient.environment.isPreviewMode
245-
else {
246-
return completion(.failure(.unknownError(domain: "", code: 0, description: "MiniApp is not in preview mode")))
243+
guard !miniAppClient.environment.isPreviewMode else {
244+
return completion(.failure(.unknownError(domain: "", code: 0, description: "MiniApp is in preview mode")))
247245
}
248246
guard
249247
let cachedVersion = miniAppDownloader.getCachedMiniAppVersion(appId: appId, versionId: version ?? "")
@@ -276,7 +274,7 @@ class MiniAppViewHandler: NSObject {
276274
queryParams: self.queryParams
277275
)
278276
} catch {
279-
completion(.failure(.unknownError(domain: "", code: 0, description: "internal error")))
277+
completion(.failure(.unknownError(domain: "", code: 0, description: "Internal error")))
280278
}
281279
completion(.success(newWebView))
282280
}
@@ -316,7 +314,7 @@ class MiniAppViewHandler: NSObject {
316314
queryParams: self.queryParams
317315
)
318316
} catch {
319-
completion(.failure(.unknownError(domain: "", code: 0, description: "internal error")))
317+
completion(.failure(.unknownError(domain: "", code: 0, description: "Internal error")))
320318
}
321319
completion(.success(newWebView))
322320
}
@@ -345,7 +343,7 @@ class MiniAppViewHandler: NSObject {
345343
navigationView: (UIView & MiniAppNavigationDelegate)? = nil
346344
) throws {
347345
guard let messageInterface = messageDelegate else {
348-
throw MASDKError.unknownError(domain: "", code: 0, description: "no message interface provided")
346+
throw MASDKError.unknownError(domain: "", code: 0, description: "No message interface provided")
349347
}
350348

351349
webView.navigationDelegate = self

Tests/Unit/MiniAppViewTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ class MiniAppViewTests: XCTestCase {
204204
case .success:
205205
XCTFail("should not succeed")
206206
case let .failure(error):
207-
XCTAssertTrue(error.localizedDescription.contains("MiniApp is not in preview mode"))
207+
XCTAssertTrue(error.localizedDescription.contains("MiniApp is in preview mode"))
208208
}
209209
expectation.fulfill()
210210
}

0 commit comments

Comments
 (0)