import { NOTICE_TARGETS, ROOT_ROUTE_KEYS, getRoute, getRouteKeyByPath, } from "./routes.js"; const navigationResults = new Map(); const pageInstanceTokens = new WeakMap(); let navigationInFlight = null; let pageInstanceSequence = 0; const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key); const getPageInstanceToken = (page) => { if ((typeof page !== "object" || page === null) && typeof page !== "function") { return null; } let token = pageInstanceTokens.get(page); if (!token) { pageInstanceSequence += 1; token = pageInstanceSequence; pageInstanceTokens.set(page, token); } return token; }; const copyDataRecord = (name, value) => { if (!value || typeof value !== "object" || Array.isArray(value)) { throw new TypeError(`${name} 必须是对象`); } return Object.freeze({ ...value }); }; const assertScalarString = (name, value) => { if (typeof value !== "string" || value.length === 0) { throw new TypeError(`导航参数 ${name} 必须是非空字符串`); } }; const validateRouteParams = (routeKey, params, requireAll) => { const route = getRoute(routeKey); if (!route) throw new Error(`未知路由键:${routeKey}`); const normalizedParams = copyDataRecord(`${routeKey} 导航参数`, params); const allowed = new Set([...route.requiredParams, ...route.optionalParams]); for (const name of Object.keys(normalizedParams)) { if (!allowed.has(name)) { throw new Error(`${routeKey} 不接受导航参数 ${name}`); } assertScalarString(name, normalizedParams[name]); } if (requireAll) { for (const name of route.requiredParams) { if (!hasOwn(normalizedParams, name)) { throw new Error(`${routeKey} 缺少导航参数 ${name}`); } } } return { route, params: normalizedParams }; }; const encodeQuery = (entries) => entries .map(([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`) .join("&"); const buildValidatedRouteUrl = (routeKey, route, params, sourceKey = "") => { if (sourceKey !== "") { assertScalarString("sourceKey", sourceKey); if (!route.allowedSources.includes(sourceKey)) { throw new Error(`${sourceKey} 不能进入 ${routeKey}`); } } const entries = [...route.requiredParams, ...route.optionalParams] .filter((name) => hasOwn(params, name)) .map((name) => [name, params[name]]); if (sourceKey !== "") entries.push(["sourceKey", sourceKey]); const query = encodeQuery(entries); return query ? `${route.path}?${query}` : route.path; }; export const buildRouteUrl = (routeKey, params = {}, sourceKey = "") => { const validated = validateRouteParams(routeKey, params, true); return buildValidatedRouteUrl( routeKey, validated.route, validated.params, sourceKey, ); }; const getPagePath = (page) => { const rawPath = page?.route || page?.$page?.route || page?.$page?.fullPath; if (typeof rawPath !== "string" || rawPath.length === 0) return ""; return rawPath.split("?")[0]; }; const getPageParams = (page, route) => { const params = Object.create(null); const routeFields = [...route.requiredParams, ...route.optionalParams]; const candidates = [page?.$page?.query, page?.$page?.options, page?.options]; // UniApp 各端暴露页面参数的位置不同。只投影注册表声明字段,避免把 // sourceKey 等传输字段误当成目标业务参数;后出现的容器拥有更高优先级, // 但空的顶层 options 不会遮蔽 $page.query/$page.options。 for (const candidate of candidates) { if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate)) { continue; } for (const name of routeFields) { if (hasOwn(candidate, name)) params[name] = candidate[name]; } } return params; }; const readPageStack = () => { if (typeof getCurrentPages !== "function") { throw new Error("当前运行环境不支持页面栈读取"); } const stack = getCurrentPages(); if (!Array.isArray(stack)) throw new Error("页面栈格式无效"); return stack; }; const getPageRouteKey = (page) => getRouteKeyByPath(getPagePath(page)); const assertActualSource = (routeKey, sourceKey) => { const route = getRoute(routeKey); if (!route) throw new Error(`未知路由键:${routeKey}`); assertScalarString("sourceKey", sourceKey); if (!route.allowedSources.includes(sourceKey)) { throw new Error(`${sourceKey} 不能进入 ${routeKey}`); } const stack = readPageStack(); const actualSource = stack.length > 0 ? getPageRouteKey(stack[stack.length - 1]) : null; if (actualSource !== sourceKey) { throw new Error( `当前真实页面 ${actualSource || "UNKNOWN"} 与声明来源 ${sourceKey} 不一致`, ); } }; const isCurrentTarget = (routeKey, params) => { const stack = readPageStack(); if (stack.length === 0) return false; const currentPage = stack[stack.length - 1]; if (getPageRouteKey(currentPage) !== routeKey) return false; const route = getRoute(routeKey); const currentParams = getPageParams(currentPage, route); for (const name of [...route.requiredParams, ...route.optionalParams]) { const targetHasParam = hasOwn(params, name); const currentHasParam = hasOwn(currentParams, name); if (targetHasParam !== currentHasParam) return false; if (targetHasParam && currentParams[name] !== params[name]) return false; } return true; }; const runUniNavigation = ( key, invoke, onStart = null, onAbort = null, onSuccess = null, ) => { if (navigationInFlight?.key === key) return navigationInFlight.promise; if (navigationInFlight) return Promise.resolve(false); let resolvePromise; let rejectPromise; let settled = false; const promise = new Promise((resolve, reject) => { resolvePromise = resolve; rejectPromise = reject; }); const flight = { key, promise }; navigationInFlight = flight; const releaseFlight = () => { if (navigationInFlight === flight) navigationInFlight = null; }; const resolveFlight = () => { if (settled) return; settled = true; releaseFlight(); resolvePromise(true); }; const rejectFlight = (error) => { if (settled) return; settled = true; let rejection = error; try { onAbort?.(); } catch (abortError) { rejection = abortError; } releaseFlight(); rejectPromise(rejection); }; try { onStart?.(); invoke({ success: () => { try { onSuccess?.(); resolveFlight(); } catch (error) { rejectFlight(error); } }, fail: (error) => rejectFlight( new Error(error?.errMsg || "页面跳转失败"), ), complete: () => { if (!settled) { rejectFlight(new Error("页面跳转未返回 success 或 fail")); } }, }); } catch (error) { rejectFlight(error); } return promise; }; const asNavigationPromise = (execute) => { try { return execute(); } catch (error) { return Promise.reject(error); } }; const validateNavigationResult = (routeKey, result) => { const route = getRoute(routeKey); if (!route) throw new Error(`未知路由键:${routeKey}`); const normalizedResult = copyDataRecord(`${routeKey} 导航结果`, result); const fields = Object.keys(normalizedResult); const allowedFields = new Set(["operation", "entityId", "refresh"]); for (const field of fields) { if (typeof field !== "string" || !allowedFields.has(field)) { throw new Error(`${routeKey} 导航结果不接受字段 ${String(field)}`); } } for (const requiredField of ["operation", "refresh"]) { if (!hasOwn(normalizedResult, requiredField)) { throw new Error(`${routeKey} 导航结果缺少 ${requiredField}`); } } assertScalarString("operation", normalizedResult.operation); if (!route.resultOperations.includes(normalizedResult.operation)) { throw new Error(`${routeKey} 不接受结果 operation=${normalizedResult.operation}`); } if (hasOwn(normalizedResult, "entityId")) { assertScalarString("entityId", normalizedResult.entityId); } if (typeof normalizedResult.refresh !== "boolean") { throw new TypeError("导航结果 refresh 必须是布尔值"); } return Object.freeze({ operation: normalizedResult.operation, ...(hasOwn(normalizedResult, "entityId") ? { entityId: normalizedResult.entityId } : {}), refresh: normalizedResult.refresh, }); }; const runWithNavigationResult = ( routeKey, targetPage, sourcePage, targetParams, result, key, invoke, ) => { const envelope = Object.seal({ targetPageToken: getPageInstanceToken(targetPage), sourcePageToken: getPageInstanceToken(sourcePage), targetParams, result, }); let started = false; const rollback = () => { if (navigationResults.get(routeKey) !== envelope) return; navigationResults.delete(routeKey); }; const bindCreatedTarget = () => { if (navigationResults.get(routeKey) !== envelope) return; const stack = readPageStack(); const currentPage = stack.length > 0 ? stack[stack.length - 1] : null; if (getPageRouteKey(currentPage) !== routeKey) { throw new Error(`${routeKey} 导航成功后未找到规范目标页`); } const currentPageToken = getPageInstanceToken(currentPage); const route = getRoute(routeKey); const currentParams = validateRouteParams( routeKey, getPageParams(currentPage, route), true, ).params; const paramsMatch = routeParamsEqual( route, currentParams, envelope.targetParams, ); if (envelope.targetPageToken !== null) { if (currentPageToken !== envelope.targetPageToken || !paramsMatch) { throw new Error(`${routeKey} 导航成功后的目标实例或参数不匹配`); } return; } if (currentPageToken === envelope.sourcePageToken || !paramsMatch) { throw new Error(`${routeKey} 导航成功后的目标实例或参数不匹配`); } envelope.targetPageToken = currentPageToken; }; const promise = runUniNavigation( key, invoke, () => { started = true; // 任意新完成流程取得全局转场锁后,所有旧结果都已经错过各自目标页的 // onShow 生命周期,必须先整体淘汰再写入当前结果;当前转场失败时只删除 // 当前 envelope,不复活任何路由的陈旧结果。 navigationResults.clear(); navigationResults.set(routeKey, envelope); }, rollback, bindCreatedTarget, ); if (!started) return promise; // 清理副作用挂在原 Promise 上,但仍返回原 Promise,保证相同语义调用的 // Promise 身份一致;失败处理分支吞掉派生链结果,不改变调用方收到的拒绝。 promise.then( (completed) => { if (!completed) rollback(); }, () => rollback(), ); return promise; }; const pushPage = (url) => runUniNavigation(`push:${url}`, (callbacks) => uni.navigateTo({ url, ...callbacks })); const activateExistingSinglePage = (routeKey, params, url) => { const stack = readPageStack(); const targetIndexes = []; for (let index = stack.length - 2; index >= 0; index -= 1) { if (getPageRouteKey(stack[index]) === routeKey) { targetIndexes.push(index); } } if (targetIndexes.length > 1) { const error = new Error("T03_STACK_CONFLICT:页面栈中存在多个成员页实例"); error.code = "T03_STACK_CONFLICT"; throw error; } const targetIndex = targetIndexes[0] ?? -1; if (targetIndex < 0) return pushPage(url); // 成员档案是当前唯一 single 页面。复用实例前必须锁定家谱上下文;若跨家谱强行 // 复用,成员轨迹和一次性结果都会串到错误领域,因此明确失败而不是猜测回退。 const existingParams = validateRouteParams( routeKey, getPageParams(stack[targetIndex], getRoute(routeKey)), true, ).params; if (existingParams.genealogyId !== params.genealogyId) { const error = new Error("T03_CONTEXT_CONFLICT:既有成员页属于其他家谱"); error.code = "T03_CONTEXT_CONFLICT"; throw error; } const navigationResult = validateNavigationResult(routeKey, { operation: "member-open-requested", entityId: params.personId, refresh: false, }); const delta = stack.length - 1 - targetIndex; const transitionKey = `single:${routeKey}:${delta}:${params.personId}`; return runWithNavigationResult( routeKey, stack[targetIndex], stack[stack.length - 1], existingParams, navigationResult, transitionKey, (callbacks) => uni.navigateBack({ delta, ...callbacks }), ); }; export const openPage = (routeKey, params = {}, sourceKey = "") => asNavigationPromise(() => { const validated = validateRouteParams(routeKey, params, true); const url = buildValidatedRouteUrl( routeKey, validated.route, validated.params, sourceKey, ); assertActualSource(routeKey, sourceKey); if (isCurrentTarget(routeKey, validated.params)) return Promise.resolve(false); if (validated.route.kind === "single") { return activateExistingSinglePage(routeKey, validated.params, url); } return pushPage(url); }); const confirmExternalSiteContentTarget = (targetUrl) => new Promise((resolve, reject) => { const host = targetUrl.match(/^https:\/\/([^/?#]+)/)?.[1] || "外部网站"; uni.showModal({ title: "即将离开应用", content: `将打开外部网站 ${host},请确认链接来源可信。`, confirmText: "继续访问", cancelText: "取消", success: ({ confirm }) => resolve(confirm === true), fail: reject, }); }); export const openSiteContentTarget = async (targetUrl, onExternalFailure = null) => { if ( typeof targetUrl !== "string" || targetUrl.trim() !== targetUrl || !( /^\/(?!\/)[^\s]*$/.test(targetUrl) || /^https:\/\/[^\s/?#]+(?:[/?#][^\s]*)?$/.test(targetUrl) ) ) { return Promise.reject(new TypeError("站点内容跳转地址无效")); } if (onExternalFailure !== null && typeof onExternalFailure !== "function") { return Promise.reject(new TypeError("外部链接失败回调必须是函数")); } if (targetUrl.startsWith("/")) { return runUniNavigation(`site-content:${targetUrl}`, (callbacks) => uni.navigateTo({ url: targetUrl, ...callbacks })); } if (!await confirmExternalSiteContentTarget(targetUrl)) return false; if (typeof plus !== "undefined" && typeof plus.runtime?.openURL === "function") { return new Promise((resolve, reject) => { try { plus.runtime.openURL(targetUrl, onExternalFailure || (() => {})); resolve(true); } catch (error) { reject(error); } }); } if (typeof window !== "undefined" && typeof window.location?.assign === "function") { window.location.assign(targetUrl); return Promise.resolve(true); } return Promise.reject(new Error("当前运行环境不支持打开外部链接")); }; export const goRoot = (routeKey, params = {}) => asNavigationPromise(() => { if (!ROOT_ROUTE_KEYS.includes(routeKey)) { throw new Error(`${routeKey} 不是根语义`); } const validated = validateRouteParams(routeKey, params, true); const url = buildValidatedRouteUrl( routeKey, validated.route, validated.params, ); if (isCurrentTarget(routeKey, validated.params)) return Promise.resolve(false); return runUniNavigation(`root:${url}`, (callbacks) => uni.reLaunch({ url, ...callbacks })); }); export const openNoticeTarget = (targetType, params, sourceKey = "N02") => asNavigationPromise(() => { assertScalarString("通知目标类型", targetType); assertScalarString("通知来源", sourceKey); if (!hasOwn(NOTICE_TARGETS, targetType)) { throw new Error(`未知通知目标:${targetType}`); } if (!["N01", "N02"].includes(sourceKey)) { throw new Error(`${sourceKey} 不能进入通知目标`); } const target = NOTICE_TARGETS[targetType]; const normalizedParams = copyDataRecord("通知目标参数", params); const expectedParams = new Set(target.params); for (const name of Object.keys(normalizedParams)) { if (!expectedParams.has(name)) { throw new Error(`${targetType} 通知目标不接受参数 ${name}`); } assertScalarString(name, normalizedParams[name]); } for (const name of target.params) { if (!hasOwn(normalizedParams, name)) { throw new Error(`${targetType} 通知目标缺少参数 ${name}`); } } return target.routeKey === "G01" ? goRoot(target.routeKey, normalizedParams) : openPage(target.routeKey, normalizedParams, sourceKey); }); const collectParentParams = (childRoute, childParams, parentRoute) => { const parentParams = {}; const parentFields = [ ...parentRoute.requiredParams, ...parentRoute.optionalParams, ]; for (const name of parentFields) { if ( hasOwn(childParams, name) && typeof childParams[name] === "string" && childParams[name].length > 0 ) { parentParams[name] = childParams[name]; } } for (const [parentParam, childParam] of Object.entries(childRoute.parentParamMap)) { if ( hasOwn(childParams, childParam) && typeof childParams[childParam] === "string" && childParams[childParam].length > 0 ) { parentParams[parentParam] = childParams[childParam]; } } return parentParams; }; const runParentFallback = (routeKey, params) => { let childRoute = getRoute(routeKey); let childParams = params; // 深链没有可信历史。逐级只继承注册表声明的同名字段和 parentParamMap;任何 // 必填字段不足的中间页都不能伪造,继续上溯到首个可合法构造的父语义。 while (childRoute?.parent) { const parentKey = childRoute.parent; const parentRoute = getRoute(parentKey); const parentParams = collectParentParams(childRoute, childParams, parentRoute); const hasRequiredParams = parentRoute.requiredParams.every((name) => hasOwn(parentParams, name)); if (hasRequiredParams) { const url = buildRouteUrl(parentKey, parentParams); if (ROOT_ROUTE_KEYS.includes(parentKey)) { return runUniNavigation(`fallback-root:${url}`, (callbacks) => uni.reLaunch({ url, ...callbacks })); } return runUniNavigation(`fallback-replace:${url}`, (callbacks) => uni.redirectTo({ url, ...callbacks })); } childRoute = parentRoute; childParams = parentParams; } return Promise.resolve(false); }; export const goBack = () => asNavigationPromise(() => { const stack = readPageStack(); if (stack.length === 0) throw new Error("当前页面栈为空"); if (stack.length > 1) { return runUniNavigation("back:1", (callbacks) => uni.navigateBack({ delta: 1, ...callbacks })); } const currentPage = stack[0]; const currentRouteKey = getPageRouteKey(currentPage); if (!currentRouteKey) throw new Error("当前页面不在活动路由注册表中"); if (ROOT_ROUTE_KEYS.includes(currentRouteKey)) return Promise.resolve(false); const currentRoute = getRoute(currentRouteKey); return runParentFallback( currentRouteKey, getPageParams(currentPage, currentRoute), ); }); const findNearestPageIndex = (stack, routeKey) => { for (let index = stack.length - 2; index >= 0; index -= 1) { if (getPageRouteKey(stack[index]) === routeKey) return index; } return -1; }; const getResultTransitionKey = (result) => { if (!result) return ""; const entityKey = hasOwn(result, "entityId") ? `1:${encodeURIComponent(result.entityId)}` : "0"; return `:${encodeURIComponent(result.operation)}:${entityKey}:${result.refresh ? "1" : "0"}`; }; const routeParamsEqual = (route, left, right) => { for (const name of [...route.requiredParams, ...route.optionalParams]) { const leftHasParam = hasOwn(left, name); const rightHasParam = hasOwn(right, name); if (leftHasParam !== rightHasParam) return false; if (leftHasParam && left[name] !== right[name]) return false; } return true; }; const assertResultSourceContext = (targetRoute, targetParams, sourcePage) => { const sourceRouteKey = getPageRouteKey(sourcePage); const sourceRoute = getRoute(sourceRouteKey); if (!sourceRoute) return; const sourceParams = validateRouteParams( sourceRouteKey, getPageParams(sourcePage, sourceRoute), true, ).params; const targetFields = new Set([ ...targetRoute.requiredParams, ...targetRoute.optionalParams, ]); for (const name of [...sourceRoute.requiredParams, ...sourceRoute.optionalParams]) { if ( targetFields.has(name) && hasOwn(sourceParams, name) && hasOwn(targetParams, name) && sourceParams[name] !== targetParams[name] ) { throw new Error(`完成来源上下文 ${name} 与目标不一致`); } } }; const isImplicitRootOptionalContext = ( routeKey, route, existingParams, targetParams, name, sourcePage, ) => { if ( !ROOT_ROUTE_KEYS.includes(routeKey) || !route.optionalParams.includes(name) || hasOwn(existingParams, name) || !hasOwn(targetParams, name) ) { return false; } const sourceRouteKey = getPageRouteKey(sourcePage); const sourceRoute = getRoute(sourceRouteKey); if (!sourceRoute) return false; const sourceParams = validateRouteParams( sourceRouteKey, getPageParams(sourcePage, sourceRoute), true, ).params; return hasOwn(sourceParams, name) && sourceParams[name] === targetParams[name]; }; const returnToValidated = (routeKey, targetParams, result) => { const validatedTarget = validateRouteParams( routeKey, targetParams, result !== null, ); const route = validatedTarget.route; const normalizedTargetParams = validatedTarget.params; const stack = readPageStack(); const targetIndex = findNearestPageIndex(stack, routeKey); const sourcePage = stack.length > 0 ? stack[stack.length - 1] : null; let targetPage = null; let targetIdentity; let transitionKey; let invoke; if (targetIndex >= 0) { targetPage = stack[targetIndex]; const existingParams = getPageParams(stack[targetIndex], route); const validatedExisting = validateRouteParams(routeKey, existingParams, true); targetIdentity = validatedExisting.params; for (const [name, value] of Object.entries(normalizedTargetParams)) { if ( (!hasOwn(validatedExisting.params, name) || validatedExisting.params[name] !== value) && !isImplicitRootOptionalContext( routeKey, route, validatedExisting.params, normalizedTargetParams, name, sourcePage, ) ) { throw new Error(`${routeKey} 显式目标参数 ${name} 与最近实例不一致`); } } const delta = stack.length - 1 - targetIndex; transitionKey = `return:${routeKey}:${delta}${getResultTransitionKey(result)}`; invoke = (callbacks) => uni.navigateBack({ delta, ...callbacks }); } else { const completeTarget = validateRouteParams( routeKey, normalizedTargetParams, true, ); targetIdentity = completeTarget.params; const url = buildValidatedRouteUrl(routeKey, route, completeTarget.params); const resultKey = getResultTransitionKey(result); if (ROOT_ROUTE_KEYS.includes(routeKey)) { transitionKey = `return-root:${url}${resultKey}`; invoke = (callbacks) => uni.reLaunch({ url, ...callbacks }); } else { transitionKey = `return-replace:${url}${resultKey}`; invoke = (callbacks) => uni.redirectTo({ url, ...callbacks }); } } if (result) { assertResultSourceContext(route, targetIdentity, sourcePage); } return result ? runWithNavigationResult( routeKey, targetPage, sourcePage, targetIdentity, result, transitionKey, invoke, ) : runUniNavigation(transitionKey, invoke); }; export const returnTo = (routeKey, targetParams = {}, ...unsupportedArgs) => asNavigationPromise(() => { if (unsupportedArgs.length > 0) { throw new Error("returnTo 不接受流程结果,请使用 finishPage"); } return returnToValidated(routeKey, targetParams, null); }); export const finishPage = (routeKey, targetParams = {}, result) => asNavigationPromise(() => { const normalizedResult = validateNavigationResult(routeKey, result); return returnToValidated(routeKey, targetParams, normalizedResult); }); export const consumeNavigationResult = (routeKey) => { const route = getRoute(routeKey); if (!route) throw new Error(`未知路由键:${routeKey}`); const stack = readPageStack(); if ( stack.length === 0 || getPageRouteKey(stack[stack.length - 1]) !== routeKey ) { return null; } if (!navigationResults.has(routeKey)) return null; const envelope = navigationResults.get(routeKey); const currentPage = stack[stack.length - 1]; const currentPageToken = getPageInstanceToken(currentPage); if (envelope.targetPageToken !== null) { if (currentPageToken !== envelope.targetPageToken) return null; } else if (currentPageToken === envelope.sourcePageToken) { return null; } const currentParams = validateRouteParams( routeKey, getPageParams(currentPage, route), true, ).params; if (!routeParamsEqual(route, currentParams, envelope.targetParams)) return null; navigationResults.delete(routeKey); return envelope.result; }; const resolveBackActionFromContext = ({ transientOpen = false, internalTrail = false, dirty = false, submitting = false, }) => { if (transientOpen) return "close-transient"; if (internalTrail) return "pop-internal-trail"; if (submitting) return "block-submitting"; if (dirty) return "confirm-discard"; return "go-back"; }; const validateBackContext = (context) => { const normalizedContext = copyDataRecord("返回守卫上下文", context); for (const flag of ["transientOpen", "internalTrail", "dirty", "submitting"]) { if (hasOwn(normalizedContext, flag) && typeof normalizedContext[flag] !== "boolean") { throw new TypeError(`返回守卫状态 ${flag} 必须是布尔值`); } } return normalizedContext; }; // onBackPress 必须同步返回布尔值;而网关自己的 navigateBack 会再次触发该钩子。 // 这里统一放行网关回调来源,并把页面异步守卫从同步平台钩子中安全分离。 export const handleBackPress = (event, requestBack) => { if (event?.from === "navigateBack") return false; if (typeof requestBack !== "function") { throw new TypeError("handleBackPress 的 requestBack 必须是函数"); } void Promise.resolve() .then(requestBack) .catch((error) => console.error("页面返回守卫执行失败", error)); return true; }; export const runBackGuard = async (context = {}) => { const normalizedContext = validateBackContext(context); const action = resolveBackActionFromContext(normalizedContext); if (action === "go-back") return goBack(); const callback = normalizedContext[action]; if (typeof callback !== "function") { throw new TypeError(`返回守卫缺少 ${action} 回调`); } const outcome = await callback(); if (action !== "confirm-discard") { return true; } if (outcome !== true) return false; return goBack(); };