1+ import ajax from "@deadlyjack/ajax" ;
2+ import alert from "dialogs/alert" ;
3+ import confirm from "dialogs/confirm" ;
14import loader from "dialogs/loader" ;
25import fsOperation from "fileSystem" ;
6+ import purchaseListener from "handlers/purchase" ;
37import JSZip from "jszip" ;
48import Url from "utils/Url" ;
9+ import helpers from "utils/helpers" ;
510import constants from "./constants" ;
611import InstallState from "./installState" ;
712import loadPlugin from "./loadPlugin" ;
813
14+ /** @type {import("dialogs/loader").Loader } */
15+ let loaderDialog ;
16+ /** @type {Array<() => Promise<void>> } */
17+ let depsLoaders ;
18+
919/**
1020 * Installs a plugin.
1121 * @param {string } id
1222 * @param {string } name
1323 * @param {string } purchaseToken
24+ * @param {boolean } isDependency
1425 */
15- export default async function installPlugin ( id , name , purchaseToken ) {
16- const title = name || "Plugin" ;
17- const loaderDialog = loader . create ( title , strings . installing ) ;
26+ export default async function installPlugin (
27+ id ,
28+ name ,
29+ purchaseToken ,
30+ isDependency ,
31+ ) {
32+ if ( ! isDependency ) {
33+ loaderDialog = loader . create ( name || "Plugin" , strings . installing ) ;
34+ depsLoaders = [ ] ;
35+ }
36+
1837 let pluginDir ;
1938 let pluginUrl ;
2039 let state ;
@@ -43,7 +62,7 @@ export default async function installPlugin(id, name, purchaseToken) {
4362 }
4463
4564 try {
46- loaderDialog . show ( ) ;
65+ if ( ! isDependency ) loaderDialog . show ( ) ;
4766
4867 const plugin = await fsOperation ( pluginUrl ) . readFile (
4968 undefined ,
@@ -62,10 +81,39 @@ export default async function installPlugin(id, name, purchaseToken) {
6281 throw new Error ( strings [ "invalid plugin" ] ) ;
6382 }
6483
84+ /** @type {{ dependencies: string[] } } */
6585 const pluginJson = JSON . parse (
6686 await zip . files [ "plugin.json" ] . async ( "text" ) ,
6787 ) ;
6888
89+ if ( ! isDependency && pluginJson . dependencies ) {
90+ const manifests = await resolveDepsManifest ( pluginJson . dependencies ) ;
91+
92+ let titleText ;
93+ if ( manifests . length > 1 ) {
94+ titleText = "Acode wants to install the following dependencies:" ;
95+ } else {
96+ titleText = "Acode wants to install the following dependency:" ;
97+ }
98+
99+ const shouldInstall = await confirm (
100+ "Installer Notice" ,
101+ titleText +
102+ "<br /><br />" +
103+ manifests . map ( ( value ) => value . name ) . join ( ", " ) ,
104+ true ,
105+ ) ;
106+
107+ if ( shouldInstall ) {
108+ for ( const manifest of manifests ) {
109+ const hasError = await resolveDep ( manifest ) ;
110+ if ( hasError ) throw new Error ( strings . failed ) ;
111+ }
112+ } else {
113+ return ;
114+ }
115+ }
116+
69117 if ( ! pluginDir ) {
70118 pluginJson . source = pluginUrl ;
71119 id = pluginJson . id ;
@@ -110,7 +158,18 @@ export default async function installPlugin(id, name, purchaseToken) {
110158
111159 // Wait for all files to be processed
112160 await Promise . allSettled ( promises ) ;
113- await loadPlugin ( id , true ) ;
161+
162+ if ( isDependency ) {
163+ depsLoaders . push ( async ( ) => {
164+ await loadPlugin ( id , true ) ;
165+ } ) ;
166+ } else {
167+ for ( const loader of depsLoaders ) {
168+ await loader ( ) ;
169+ }
170+ await loadPlugin ( id , true ) ;
171+ }
172+
114173 await state . save ( ) ;
115174 deleteRedundantFiles ( pluginDir , state ) ;
116175 }
@@ -128,7 +187,9 @@ export default async function installPlugin(id, name, purchaseToken) {
128187 }
129188 throw err ;
130189 } finally {
131- loaderDialog . destroy ( ) ;
190+ if ( ! isDependency ) {
191+ loaderDialog . destroy ( ) ;
192+ }
132193 }
133194}
134195
@@ -160,6 +221,112 @@ async function createFileRecursive(parent, dir) {
160221 await createFileRecursive ( newParent , dir ) ;
161222 }
162223}
224+
225+ /**
226+ * Resolves Dependencies Manifest with given ids.
227+ * @param {string[] } deps dependencies
228+ */
229+ async function resolveDepsManifest ( deps ) {
230+ const resolved = [ ] ;
231+ for ( const dependency of deps ) {
232+ const remoteDependency = await fsOperation (
233+ constants . API_BASE ,
234+ `plugin/${ dependency } ` ,
235+ )
236+ . readFile ( "json" )
237+ . catch ( ( ) => null ) ;
238+
239+ if ( ! remoteDependency )
240+ throw new Error ( `Unknown plugin dependency: ${ dependency } ` ) ;
241+
242+ const version = await getInstalledPluginVersion ( remoteDependency . id ) ;
243+ if ( remoteDependency ?. version === version ) continue ;
244+
245+ if ( remoteDependency . dependencies ) {
246+ const manifests = await resolveDepsManifest (
247+ remoteDependency . dependencies ,
248+ ) ;
249+ resolved . push ( manifests ) ;
250+ }
251+
252+ resolved . push ( remoteDependency ) ;
253+ }
254+
255+ /**
256+ *
257+ * @param {string } id
258+ * @returns {Promise<string> } plugin version
259+ */
260+ async function getInstalledPluginVersion ( id ) {
261+ if ( await fsOperation ( PLUGIN_DIR , id ) . exists ( ) ) {
262+ const plugin = await fsOperation ( PLUGIN_DIR , id , "plugin.json" ) . readFile (
263+ "json" ,
264+ ) ;
265+ return plugin . version ;
266+ }
267+ }
268+
269+ return resolved ;
270+ }
271+
272+ /** Resolve dependency
273+ * @param {object } manifest
274+ * @returns {Promise<boolean> } has error
275+ */
276+ async function resolveDep ( manifest ) {
277+ let purchaseToken ;
278+ let product ;
279+ let isPaid = false ;
280+
281+ isPaid = manifest . price > 0 ;
282+ [ product ] = await helpers . promisify ( iap . getProducts , [ manifest . sku ] ) ;
283+ if ( product ) {
284+ const purchase = await getPurchase ( product . productId ) ;
285+ purchaseToken = purchase ?. purchaseToken ;
286+ }
287+
288+ if ( isPaid && ! purchaseToken ) {
289+ if ( ! product ) throw new Error ( "Product not found" ) ;
290+ const apiStatus = await helpers . checkAPIStatus ( ) ;
291+
292+ if ( ! apiStatus ) {
293+ alert ( strings . error , strings . api_error ) ;
294+ return true ;
295+ }
296+
297+ iap . setPurchaseUpdatedListener ( ...purchaseListener ( onpurchase , onerror ) ) ;
298+ loaderDialog . setMessage ( strings [ "loading..." ] ) ;
299+ await helpers . promisify ( iap . purchase , product . json ) ;
300+
301+ async function onpurchase ( e ) {
302+ const purchase = await getPurchase ( product . productId ) ;
303+ await ajax . post ( Url . join ( constants . API_BASE , "plugin/order" ) , {
304+ data : {
305+ id : manifest . id ,
306+ token : purchase ?. purchaseToken ,
307+ package : BuildInfo . packageName ,
308+ } ,
309+ } ) ;
310+ purchaseToken = purchase ?. purchaseToken ;
311+ }
312+
313+ async function onerror ( error ) {
314+ throw error ;
315+ }
316+ }
317+
318+ loaderDialog . setMessage (
319+ `${ strings . installing . replace ( "..." , "" ) } ${ manifest . name } ...` ,
320+ ) ;
321+ await installPlugin ( manifest . id , undefined , purchaseToken , true ) ;
322+
323+ async function getPurchase ( sku ) {
324+ const purchases = await helpers . promisify ( iap . getPurchases ) ;
325+ const purchase = purchases . find ( ( p ) => p . productIds . includes ( sku ) ) ;
326+ return purchase ;
327+ }
328+ }
329+
163330/**
164331 *
165332 * @param {string } dir
@@ -181,7 +348,7 @@ async function listFileRecursive(dir, files) {
181348 * @param {Record<string, boolean> } files
182349 */
183350async function deleteRedundantFiles ( pluginDir , state ) {
184- /** @type string[] */
351+ /** @type { string[] } */
185352 let files = [ ] ;
186353 await listFileRecursive ( pluginDir , files ) ;
187354
0 commit comments