@@ -54,3 +54,196 @@ function pulse(state) {
5454
5555 pulseState = requestAnimationFrame ( animate ) ;
5656}
57+
58+
59+
60+ // INSTALLER
61+ let installer = ( function ( ) {
62+ // Config defaults (change if you need)
63+ const PREFLIGHT_URL = "https://ipa.s0n1c.ca/preflight" ;
64+ const INSTALL_BASE = "https://ipa.s0n1c.ca" ;
65+ const DEFAULT_REPO = "ProStore-iOS/ProStore" ; // repo to inspect by default
66+
67+ // internal state
68+ let _progress = 0 ; // 0..100
69+ let _finished = false ;
70+ let _lastError = null ;
71+ let _installData = null ; // response from preflight (contains id, name, version, etc.)
72+ let _chosenAsset = null ; // metadata about chosen release asset
73+ let _releaseInfo = null ; // chosen release object
74+
75+ // helpers
76+ function _setProgress ( n ) {
77+ _progress = Math . max ( 0 , Math . min ( 100 , Math . round ( n ) ) ) ;
78+ }
79+
80+ function _chooseAssetFromRelease ( release ) {
81+ const assets = Array . isArray ( release . assets ) ? release . assets : [ ] ;
82+ // candidate .ipa assets
83+ const ipaAssets = assets . filter ( a => a && a . name && / \. i p a $ / i. test ( a . name ) ) ;
84+ if ( ! ipaAssets . length ) return null ;
85+ // prefer those with "signed" in the filename
86+ const signed = ipaAssets . filter ( a => / s i g n e d / i. test ( a . name ) ) ;
87+ if ( signed . length ) return signed [ 0 ] ;
88+ // fallback: return the first ipa asset
89+ return ipaAssets [ 0 ] ;
90+ }
91+
92+ async function _fetchJson ( url , opts = { } ) {
93+ const res = await fetch ( url , opts ) ;
94+ if ( ! res . ok ) {
95+ const text = await res . text ( ) . catch ( ( ) => "" ) ;
96+ const msg = `HTTP ${ res . status } ${ text ? " - " + text : "" } ` ;
97+ const e = new Error ( msg ) ;
98+ e . status = res . status ;
99+ throw e ;
100+ }
101+ return res . json ( ) ;
102+ }
103+
104+ return {
105+ // Return percentage (integer)
106+ getStatus : function ( ) {
107+ return _progress ;
108+ } ,
109+
110+ // Begin the install flow (auto-select latest release & preferred asset)
111+ // options: { repo: "owner/repo", token: "GITHUB_PAT (optional)", preferPrerelease: false }
112+ // Returns a Promise that resolves to the final preflight response object when finished.
113+ beginInstall : async function ( options = { } ) {
114+ const repo = options . repo || DEFAULT_REPO ;
115+ const token = options . token || null ;
116+ const preferPrerelease = ! ! options . preferPrerelease ;
117+
118+ _finished = false ;
119+ _lastError = null ;
120+ _installData = null ;
121+ _chosenAsset = null ;
122+ _releaseInfo = null ;
123+ _setProgress ( 5 ) ;
124+
125+ try {
126+ _setProgress ( 12 ) ;
127+ // get releases (latest first)
128+ const apiUrl = `https://api.github.com/repos/${ repo } /releases?per_page=50` ;
129+ const headers = {
130+ Accept : "application/vnd.github.v3+json" ,
131+ ...( token ? { Authorization : `token ${ token } ` } : { } )
132+ } ;
133+
134+ _setProgress ( 18 ) ;
135+
136+ const releases = await _fetchJson ( apiUrl , { headers } ) ;
137+ if ( ! Array . isArray ( releases ) || releases . length === 0 ) {
138+ throw new Error ( "No releases returned from GitHub." ) ;
139+ }
140+
141+ // filter out drafts; optionally include prereleases if user requested
142+ const visible = releases
143+ . filter ( r => ! r . draft )
144+ . filter ( r => preferPrerelease ? true : ! r . prerelease )
145+ . sort ( ( a , b ) => new Date ( b . created_at ) - new Date ( a . created_at ) ) ;
146+
147+ if ( ! visible . length ) {
148+ // if strict filter removed everything, try less strict (non-draft)
149+ const fallback = releases . filter ( r => ! r . draft ) . sort ( ( a , b ) => new Date ( b . created_at ) - new Date ( a . created_at ) ) ;
150+ if ( ! fallback . length ) throw new Error ( "No suitable releases found (all drafts?)." ) ;
151+ _releaseInfo = fallback [ 0 ] ;
152+ } else {
153+ _releaseInfo = visible [ 0 ] ; // latest
154+ }
155+
156+ _setProgress ( 30 ) ;
157+
158+ // pick an asset (prefer "signed" .ipa)
159+ const chosen = _chooseAssetFromRelease ( _releaseInfo ) ;
160+ if ( ! chosen ) throw new Error ( "No .ipa assets found in the latest release." ) ;
161+ _chosenAsset = chosen ;
162+
163+ _setProgress ( 45 ) ;
164+
165+ // Prepare signing using preflight endpoint (same as original flow)
166+ const ipaUrl = chosen . browser_download_url ;
167+ if ( ! ipaUrl ) throw new Error ( "Chosen asset has no browser_download_url." ) ;
168+
169+ _setProgress ( 60 ) ;
170+
171+ const resp = await fetch ( PREFLIGHT_URL , {
172+ method : "POST" ,
173+ headers : { "Content-Type" : "application/json" } ,
174+ body : JSON . stringify ( { url : ipaUrl } )
175+ } ) ;
176+
177+ if ( ! resp . ok ) {
178+ const t = await resp . text ( ) . catch ( ( ) => "" ) ;
179+ throw new Error ( `Signing service error HTTP ${ resp . status } ${ t } ` ) ;
180+ }
181+
182+ const data = await resp . json ( ) ;
183+ if ( ! data || ! data . id ) throw new Error ( "Signing/preflight response did not include an id." ) ;
184+
185+ _installData = data ;
186+ _setProgress ( 90 ) ;
187+
188+ // finalise
189+ _finished = true ;
190+ _setProgress ( 100 ) ;
191+
192+ return data ; // contains id, name, version, etc.
193+ } catch ( err ) {
194+ _lastError = err ;
195+ _finished = false ;
196+ _setProgress ( 0 ) ;
197+ throw err ;
198+ }
199+ } ,
200+
201+ // When the flow is finished, returns the itms-services link string (or null if not finished).
202+ // The returned link uses the same install path as the original flow:
203+ // itms-services://?action=download-manifest&url=<encoded INSTALL_BASE>/<id>/install
204+ getInstallLink : function ( ) {
205+ if ( ! _finished || ! _installData || ! _installData . id ) return null ;
206+ const manifestUrl = `${ INSTALL_BASE } /${ _installData . id } /install` ;
207+ return `itms-services://?action=download-manifest&url=${ encodeURIComponent ( manifestUrl ) } ` ;
208+ } ,
209+
210+ // Optional helpers for debugging / info
211+ getChosenAsset : function ( ) {
212+ return _chosenAsset ;
213+ } ,
214+ getReleaseInfo : function ( ) {
215+ return _releaseInfo ;
216+ } ,
217+ getLastError : function ( ) {
218+ return _lastError ;
219+ }
220+ } ;
221+ } ) ( ) ;
222+
223+ function install ( ) {
224+ pulse ( true ) ;
225+ loadingImg ( 0 ) ;
226+
227+ // start polling every 0.5s immediately
228+ const interval = setInterval ( ( ) => {
229+ const status = installer . getStatus ( ) ; // % progress
230+ console . log ( "Progress:" , status , "%" ) ;
231+ loadingImg ( status ) ;
232+
233+ // stop when finished
234+ if ( status >= 100 || installer . isFinished ) {
235+ clearInterval ( interval ) ;
236+ console . log ( "Install finished!" ) ;
237+ console . log ( installer . getInstallLink ( ) ) ;
238+ pulse ( false ) ;
239+ }
240+ } , 500 ) ;
241+
242+ // start the install
243+ installer . beginInstall ( { repo : "ProStore-iOS/ProStore" , token : null } )
244+ . catch ( err => {
245+ console . error ( "Install failed:" , err ) ;
246+ clearInterval ( interval ) ;
247+ pulse ( false ) ;
248+ } ) ;
249+ }
0 commit comments