Prevent Default Permission Popups and Use Custom Ones

I’m using the 8th Wall Niantic Studio web editor, and I want to disable the default permission popups and replace them with my own custom popups. However, the project doesn’t include any head.html or body.html files that I can edit. Even if I create these files, they aren’t automatically injected into the final HTML output. Is there any way to achieve this?

The easiest way would be to inject CSS that overrides the classnames used in the prompts.

https://www.8thwall.com/docs/legacy/guides/advanced-topics/load-screen/

This is how I create HTML in studio:

// onboardingScreen.js
// Builds a simple modal-style onboarding card prompting for gyroscope permission.
// ✅ Returns a detached DOM node; you decide where to insert it
// ✅ Keeps your existing IDs so other code keeps working
// ✅ Adds basic accessibility (role="dialog", aria-modal, labels)
// 🔧 Usage:
//    const ui = CreateOnboardingScreen()
//    document.body.appendChild(ui)
//    // Add your click handler elsewhere: document.getElementById('onboardingButton')?.addEventListener(...)

function CreateOnboardingScreen() {
  const container = document.createElement('div')
  container.id = 'onboardingScreen'
  container.setAttribute('role', 'dialog')
  container.setAttribute('aria-modal', 'true')
  container.setAttribute('aria-labelledby', 'onboardingTitle')
  container.setAttribute('aria-describedby', 'onboardingDesc')

  container.innerHTML = `
    <div id="onboardingPopup">
      <p id="onboardingTitle">This Game Uses Gyro!</p>
      <p id="onboardingDesc">Please accept the permissions to continue.</p>
      <button id="onboardingButton" type="button">Continue</button>
    </div>
  `
  return container
}

export { CreateOnboardingScreen }

and then in another script i add the html to the scene and create event listeners

// ui-controller.js
// Centralized UI flow: loading → menu → prep → game → gameOver with gyro permission gate.
// ✅ Build/attach onboarding UI and request gyroscope permission on user gesture
// ✅ Loading bar blends asset progress (0–90%) + extra delay (10%) then auto-To Menu
// ✅ Smooth fade-to-black transition into game; toggles UI visibility and parenting
// ✅ Mute button syncs with audioManager via 'Mute Pressed'
// 🔊 Listens: 'Score Updated', 'Desired Camera', UI clicks on play/restart/mute
// 📦 Depends on: @8thwall/ecs (Ui/Hidden/Disabled/Position, events), requestGyroscopePermission, CreateOnboardingScreen

import * as ecs from '@8thwall/ecs'
import { requestGyroscopePermission } from './gyro-permissions'
import { CreateOnboardingScreen } from './createUI'
import './index.css'

ecs.registerComponent({
  name: 'uiController',

  // ───────────────────────────────── Schema
  schema: {
    gyroContinueButton: ecs.eid,
    gyroContinueDiv: ecs.eid,
    scoreText: ecs.eid,
    loadingBarFill: ecs.eid,
    loadingDelay: ecs.f32,
    loadingUi: ecs.eid,
    menuUi: ecs.eid,
    playButton: ecs.eid,
    blackScreen: ecs.eid,
    platforms: ecs.eid,
    scoreUI: ecs.eid,
    player: ecs.eid,
    zOffset: ecs.eid,
    gameOverUI: ecs.eid,
    gameOverScore: ecs.eid,
    gameOverHighScore: ecs.eid,
    restartButton: ecs.eid,
    muteButton: ecs.eid,
    // @asset
    muteImage: ecs.string,
    // @asset
    unmuteImage: ecs.string,
  },
  schemaDefaults: {
    loadingDelay: 3, // seconds
  },

  // ─────────────────────────────── Runtime data
  data: {
    muted: ecs.boolean,
  },

  // ─────────────────────────────── Add (init once per entity)
  add: (world, component) => {
    component.data.muted = false
  },

  // ─────────────────────────────── State Machine
  stateMachine: ({ world, eid, schemaAttribute, dataAttribute }) => {
    const sRef = () => schemaAttribute.get(eid)
    const dRef = () => dataAttribute.get(eid)

    const toMenu = ecs.defineTrigger()
    const toGame = ecs.defineTrigger()

    let currentScore = 0
    let timePassedMs = 0
    let fadeOffTime = 0
    let fadeOnTime = 0
    let blackScreenTime = 0
    let delayLoadPercent = 0
    let firstAssetsLoadedEvent = true
    let blacked = false
    let totalLoad = 0

    const extraLoadTimeMs = sRef().loadingDelay * 1000

    // Detached container for onboarding; only shown on Play
    const uiContainer = document.createElement('div')
    uiContainer.style.display = 'none'
    const gyroCard = CreateOnboardingScreen() // contains #onboardingButton
    uiContainer.appendChild(gyroCard)
    document.body.appendChild(uiContainer)

    const fadeOpacity = (alpha) => {
      ecs.Ui.mutate(world, sRef().blackScreen, (c) => {
        c.backgroundOpacity = alpha
      })
    }

    const handleClick = async () => {
      // Must be invoked by direct user gesture
      const ans = await requestGyroscopePermission()
      world.events.dispatch(world.events.globalId, 'Play Music') // kick BGM
      uiContainer.style.display = 'none'

      const tgt = world.events.globalId
      if (ans === 'Granted') world.events.dispatch(tgt, 'Permission Accepted')
      else if (ans === 'Denied') world.events.dispatch(tgt, 'Permission Denied')
      else if (ans === 'Not Supported') world.events.dispatch(tgt, 'Permission Not Supported')
      else world.events.dispatch(tgt, 'Permission Error')

      const btn = document.getElementById('onboardingButton')
      if (btn) btn.removeEventListener('click', handleClick)
    }

    const setGameOverUi = (payload) => {
      ecs.Position.mutate(world, sRef().gameOverUI, (c) => {
        c.y = payload.data.position
      })
    }

    const setScore = (payload) => {
      const score = Number(payload?.data?.score ?? payload?.score ?? 0)
      ecs.Ui.mutate(world, sRef().scoreText, (cursor) => {
        cursor.text = String(score)
      })
      currentScore = score
    }

    const toggleMute = () => {
      const nowMuted = !dRef().muted
      dataAttribute.cursor(eid).muted = nowMuted
      ecs.Ui.mutate(world, sRef().muteButton, (cursor) => {
        cursor.image = nowMuted ? sRef().muteImage : sRef().unmuteImage
      })
      world.events.dispatch(world.events.globalId, 'Mute Pressed')
    }

    // ───────────────────────────── States
    ecs.defineState('loading').initial()
      .onTick(() => {
        const { pending, complete } = ecs.assets.getStatistics()
        const progress = complete / (pending + complete || 1)

        // 0–90% for assets, last 10% for "extra load" feel / UI prep
        let assetLoadPercent = Math.min(progress * 90, 90)

        if (progress >= 1) {
          if (firstAssetsLoadedEvent) {
            world.events.dispatch(world.events.globalId, 'Assets Loaded')
            firstAssetsLoadedEvent = false
          }
          assetLoadPercent = 90
          timePassedMs += world.time.delta
          delayLoadPercent = Math.min((timePassedMs / extraLoadTimeMs) * 10, 10)

          if (timePassedMs > extraLoadTimeMs + 100) {
            world.events.dispatch(world.events.globalId, 'To Menu')
            toMenu.trigger()
          }
        }

        totalLoad = assetLoadPercent + delayLoadPercent
        ecs.Ui.mutate(world, sRef().loadingBarFill, (cursor) => {
          cursor.width = `${totalLoad}%`
        })
      })
      .onTrigger(toMenu, 'Menu')

    ecs.defineState('Menu')
      .onEnter(() => {
        ecs.Hidden.set(world, sRef().loadingUi)
        ecs.Hidden.remove(world, sRef().menuUi)

        const btn = document.getElementById('onboardingButton')
        if (btn) btn.addEventListener('click', handleClick, { once: true })
      })
      .listen(sRef().playButton, ecs.input.UI_CLICK, () => {
        uiContainer.style.display = 'block'
        world.events.dispatch(world.events.globalId, 'Play Pressed')
      })
      .onEvent('Permission Accepted', 'prepGame', { target: world.events.globalId })

    ecs.defineState('prepGame')
      .onEnter(() => {
        fadeOffTime = 0
        fadeOnTime = 0
        blackScreenTime = 0
        blacked = false
      })
      .onTick(() => {
        const dt = world.time.delta

        if (fadeOnTime < 300) {
          fadeOpacity(fadeOnTime / 300)
          fadeOnTime += dt
          return
        }

        if (blackScreenTime < 500) {
          if (!blacked) {
            blacked = true
            fadeOpacity(1)
            ecs.Hidden.remove(world, sRef().platforms)
            ecs.Hidden.remove(world, sRef().scoreUI)
            world.setParent(sRef().player, sRef().zOffset)
            world.events.dispatch(world.events.globalId, 'Prep Game')
            ecs.Hidden.set(world, sRef().menuUi)
            ecs.Disabled.set(world, sRef().menuUi)
            ecs.Hidden.set(world, sRef().gameOverUI)
          }
          blackScreenTime += dt
          return
        }

        if (fadeOffTime < 300) {
          fadeOpacity(1 - fadeOffTime / 300)
          fadeOffTime += dt
          return
        }

        fadeOpacity(0)
        toGame.trigger()
      })
      .onTrigger(toGame, 'game')

    ecs.defineState('game')
      .onEnter(() => {
        world.events.dispatch(world.events.globalId, 'Start Game')
      })
      .listen(world.events.globalId, 'Score Updated', setScore)
      .onEvent('Game Over', 'gameOver', { target: world.events.globalId })

    ecs.defineState('gameOver')
      .onEnter(() => {
        ecs.Hidden.remove(world, sRef().gameOverUI)

        const KEY = 'botBounceHighScore'
        const finalScore = Number.isFinite(currentScore) ? currentScore : 0

        let highScore = 0
        try {
          const stored = localStorage.getItem(KEY)
          if (stored != null) {
            const parsed = parseInt(stored, 10)
            if (Number.isFinite(parsed)) highScore = parsed
          }
        } catch (e) {
          console.warn('Failed to load score history:', e)
        }

        if (finalScore > highScore) {
          highScore = finalScore
          try {
            localStorage.setItem(KEY, String(highScore))
          } catch (e) {
            console.warn('Failed to save score history:', e)
          }
        }

        ecs.Ui.mutate(world, sRef().gameOverHighScore, (c) => {
          c.text = String(highScore)
        })
        ecs.Ui.mutate(world, sRef().gameOverScore, (c) => {
          c.text = String(finalScore)
        })
      })
      .listen(world.events.globalId, 'Desired Camera', setGameOverUi)
      .onEvent(ecs.input.UI_CLICK, 'prepGame', { target: sRef().restartButton })

    // Global listeners active across states
    ecs.defineStateGroup(['game', 'gameOver', 'prepGame'])
      .listen(sRef().muteButton, ecs.input.UI_CLICK, toggleMute)
  },
})

these are specifically the lines that create the UI

const uiContainer = document.createElement(‘div’)
uiContainer.style.display = ‘none’
const gyroCard = CreateOnboardingScreen() // contains #onboardingButton
uiContainer.appendChild(gyroCard)
document.body.appendChild(uiContainer)

This topic was automatically closed 4 days after the last reply. New replies are no longer allowed.