@@ -248,9 +248,33 @@ export function makeMemoryStorage() {
248248 } ;
249249}
250250
251+ // IP-based location, used when the browser's own geolocation fails
252+ // (common: OS location services disabled). City-level accuracy is
253+ // plenty for weather watchfaces. Cached per page load.
254+ let ipLocationCache = null ;
255+ async function ipLocate ( fetchFn ) {
256+ if ( ipLocationCache ) return ipLocationCache ;
257+ const r = await fetchFn ( 'https://ipwho.is/' ) ;
258+ const j = await r . json ( ) ;
259+ if ( ! j || j . success === false || typeof j . latitude !== 'number' ) {
260+ throw new Error ( 'IP lookup returned no location' ) ;
261+ }
262+ ipLocationCache = { latitude : j . latitude , longitude : j . longitude , city : j . city } ;
263+ return ipLocationCache ;
264+ }
265+
266+ function fakePosition ( lat , lon , accuracy ) {
267+ return {
268+ coords : { latitude : lat , longitude : lon , accuracy,
269+ altitude : null , altitudeAccuracy : null , heading : null , speed : null } ,
270+ timestamp : Date . now ( ) ,
271+ } ;
272+ }
273+
251274// Browser sandbox: hidden same-origin iframe. The app JS gets the page's
252275// real fetch/XHR (patched with the proxy fallback), geolocation, etc.
253- export function makeIframeSandbox ( proxyUrl , log = ( ) => { } ) {
276+ // fixedLoc: optional {latitude, longitude} override (?loc=lat,lon).
277+ export function makeIframeSandbox ( proxyUrl , log = ( ) => { } , fixedLoc = null ) {
254278 return ( globals ) => {
255279 const frame = document . createElement ( 'iframe' ) ;
256280 frame . style . display = 'none' ;
@@ -272,28 +296,42 @@ export function makeIframeSandbox(proxyUrl, log = () => {}) {
272296 }
273297 throw e ;
274298 } ) ;
275- // Log geolocation outcomes — a silent OS-level denial otherwise
276- // looks identical to an app that never asked .
299+ // Geolocation with fallbacks: ?loc= override -> native -> IP-based.
300+ // An OS-level denial must not strand weather watchfaces .
277301 try {
278302 const geo = w . navigator . geolocation ;
279- const origGet = geo . getCurrentPosition . bind ( geo ) ;
280- geo . getCurrentPosition = ( ok , err , opts ) => {
303+ // Use the TOP page's geolocation, not the iframe's — it removes
304+ // the permissions-delegation variable, and the permission grant
305+ // is attributed to the visible page either way.
306+ const pageGeo = window . navigator . geolocation ;
307+ const origGet = pageGeo . getCurrentPosition . bind ( pageGeo ) ;
308+ const fallback = ( ok , err , cause ) => {
309+ log ( `geolocation failed (${ cause } ); trying IP-based location…` ) ;
310+ ipLocate ( w . fetch ) . then ( ( loc ) => {
311+ log ( `IP location: ~${ loc . city || 'unknown' } ` ) ;
312+ ok ( fakePosition ( loc . latitude , loc . longitude , 25000 ) ) ;
313+ } ) . catch ( ( e2 ) => {
314+ log ( 'IP location failed too: ' + e2 . message ) ;
315+ if ( err ) err ( { code : 2 , message : cause } ) ;
316+ } ) ;
317+ } ;
318+ const resolvePosition = ( ok , err , opts ) => {
281319 log ( 'app requested geolocation…' ) ;
320+ if ( fixedLoc ) {
321+ log ( `using fixed location ${ fixedLoc . latitude } ,${ fixedLoc . longitude } ` ) ;
322+ ok ( fakePosition ( fixedLoc . latitude , fixedLoc . longitude , 10 ) ) ;
323+ return ;
324+ }
282325 origGet (
283326 ( pos ) => { log ( `geolocation ok (±${ Math . round ( pos . coords . accuracy ) } m)` ) ; ok ( pos ) ; } ,
284- ( e ) => { log ( `geolocation DENIED/failed: ${ e . message } (code ${ e . code } )` ) ; if ( err ) err ( e ) ; } ,
285- opts ,
286- ) ;
287- } ;
288- const origWatch = geo . watchPosition . bind ( geo ) ;
289- geo . watchPosition = ( ok , err , opts ) => {
290- log ( 'app watching geolocation…' ) ;
291- return origWatch (
292- ( pos ) => { log ( 'geolocation update' ) ; ok ( pos ) ; } ,
293- ( e ) => { log ( `geolocation DENIED/failed: ${ e . message } (code ${ e . code } )` ) ; if ( err ) err ( e ) ; } ,
327+ ( e ) => fallback ( ok , err , `${ e . message } (code ${ e . code } )` ) ,
294328 opts ,
295329 ) ;
296330 } ;
331+ geo . getCurrentPosition = resolvePosition ;
332+ // Single-shot semantics are fine for watchfaces polling weather.
333+ geo . watchPosition = ( ok , err , opts ) => { resolvePosition ( ok , err , opts ) ; return 0 ; } ;
334+ geo . clearWatch = ( ) => { } ;
297335 } catch ( e ) { /* geolocation unavailable in this context */ }
298336 return {
299337 run : ( code ) => w . eval ( code ) ,
0 commit comments