Auroratide
Auroratide

Reputation: 2607

Transfer LocalStorage data to a new domain, despite limited support for Storage Access API

I've had a website hosted on Github Pages without a custom domain (e.g. auroratide.github.io/my-website), and I now want to move it to a custom domain (e.g. my-website.com). The website saves data into local storage.

my-website.com cannot access the local storage data from github.io, so I need to find a way to migrate the data so people don't suddenly lose everything.

I thought I was being clever by creating another github pages site, auroratide.github.io/storage-migration, whose only purpose is to be iframed by the new domain and postMessage its data to the parent window. To my dismay, an iframed website cannot access its own local storage due to State Partitioning, in other words storage is "double-keyed" against both the top domain and the iframed domain.

I then attempted to leverage the Storage Access API to request access from the iframe, and that works... in Chrome, and is unsupported in every other browser. Silly me for not checking before implementing an entire solution.


Without support from the Storage Access API, is there any way to migrate local storage to a new domain? Even though this exact question has been asked many times over the years, I'm definitely getting lost in the forest of old-knowledge vs new-knowledge.

If it's useful at all, this is the code I'm currently using in my iframed website:

function postMessage(payload) {
    parent.postMessage(payload, NEW_DOMAIN)
}

function transferStorage(storage) {
    postMessage(Object.entries(storage))
}

async function hasStorageAccess() {
    if (!document.requestStorageAccess) return true
    if (await document.hasStorageAccess()) return true

    try {
        const permission = await navigator.permissions.query({ name: "storage-access" })

        if (permission.state === "granted") {
            await document.requestStorageAccess()
            return true
        } else if (permission.state === "prompt") {
            return false
        } else if (permission.state === "denied") {
            return false
        }
    } catch (error) {
        console.warn(error)
        return false
    }

    return false
}

async function manuallyTransfer() {
    /* Imagine code here that toggles button and loader visibilities */

    const hasAccess = await hasStorageAccess()
    if (!hasAccess) {
        try {
            await document.requestStorageAccess()
        } catch (error) {
            postMessage("failed")
            return
        }
    }

    const handle = await document.requestStorageAccess({ localStorage: true })
    transferStorage(handle.localStorage)
}

async function automaticallyTransfer() {
    const hasAccess = await hasStorageAccess()
    if (hasAccess) {
        if (document.requestStorageAccess) {
            const handle = await document.requestStorageAccess({ localStorage: true })
            transferStorage(handle.localStorage)
        } else {
            transferStorage(localStorage)
        }
    } else {
        console.log("Requires manual input to migrate data.")
        const button = document.querySelector("#transfer-button")
        button.addEventListener("click", manuallyTransfer)
        button.removeAttribute("hidden")
        postMessage("manual")
    }
}

if (parent != null) {
    automaticallyTransfer()
}

Upvotes: 1

Views: 118

Answers (1)

Auroratide
Auroratide

Reputation: 2607

In the end I decided on using a user-initiated popup as suggested by IT goldman. Since a popup window is not constrained by state partitioning in the same way an iframe is, it's able to postMessage its local storage contents to the opener.

window.open(POPUP_URL, "datatransfer", "popup")
function postMessage(payload) {
    opener.postMessage(payload, TARGET_DOMAIN)
}

function transferStorage(storage) {
    postMessage(Object.entries(storage))
}

async function automaticallyTransfer() {
    try {
        transferStorage(localStorage)
    } catch (e) {
        console.error(e)
        postMessage("failed")
    } finally {
        window.close()
    }
}

if (opener != null) {
    automaticallyTransfer()
}

Upvotes: 1

Related Questions