Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(space-nuxt-base): refactor useAppBridge() #68

Merged
merged 10 commits into from
Jul 22, 2024
308 changes: 158 additions & 150 deletions space-plugins/nuxt-base/composables/useAppBridge.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,12 @@
import type { DecodedToken, PluginType } from '~/types/appBridge';

type PostMessageAction = 'tool-changed' | 'app-changed';

type ValidateMessagePayload = {
action: PostMessageAction;
event: 'validate';
tool?: string | null;
};

type BeginOAuthMessagePayload = {
action: PostMessageAction;
event: 'beginOAuth';
tool?: string | null;
redirectTo: string;
};

type CreateValidateMessagePayload = (params: {
type: PluginType;
slug: string | null;
}) => ValidateMessagePayload;

type CreateBeginOAuthMessagePayload = (params: {
type: PluginType;
slug: string | null;
redirectTo: string;
}) => BeginOAuthMessagePayload;
import type {
BeginOAuthMessagePayload,
CreateBeginOAuthMessagePayload,
CreateValidateMessagePayload,
DecodedToken,
PluginType,
PostMessageAction,
ValidateMessagePayload,
} from '~/types/appBridge';

const getPostMessageAction = (type: PluginType): PostMessageAction => {
switch (type) {
Expand All @@ -37,37 +19,108 @@ const getPostMessageAction = (type: PluginType): PostMessageAction => {
}
};

const useAppBridgeMessages = () => {
const getParentHost = () => {
const storedHost = sessionStorage.getItem(KEY_PARENT_HOST);
if (storedHost) {
return storedHost;
}
const params = new URLSearchParams(location.search);
const protocol = params.get('protocol');
const host = params.get('host');
if (!protocol || !host) {
throw new Error('Missing `protocol` or `host` in query params');
}
return `${protocol}//${host}`;
};

const getSlug = () => {
const storedSlug = sessionStorage.getItem(KEY_SLUG);
if (storedSlug) {
return storedSlug;
}
const params = new URLSearchParams(location.search);
return params.get('slug');
};

const postMessageToParent = (payload: unknown) => {
window.parent.postMessage(payload, getParentHost());
};

const useAppBridgeAuth = ({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be also extracted as type type UseAppBridge = (params: {authenticated: ()=> Promise<void>}) => void

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but this is a nit pick so no need to implement

authenticated,
}: {
authenticated: () => Promise<void>;
}) => {
const appConfig = useAppConfig();
const status = ref<'init' | 'authenticating' | 'authenticated' | 'error'>(
'init',
);

//TODO: rename to oauthStatus?
const oauth = ref<'disabled' | 'init' | 'authenticating' | 'authenticated'>(
appConfig.appBridge.oauth ? 'init' : 'disabled',
);

const error = ref<unknown>();
const origin = appConfig.appBridge.origin || 'https://app.storyblok.com';

const startOAuth = async () => {
oauth.value = 'authenticating';

const initOAuth =
new URLSearchParams(location.search).get('init_oauth') === 'true';
const init = async () => {
const isInIframe = window.top !== window.self;

const response = await $fetch('/api/_oauth', {
method: 'POST',
body: { initOAuth },
});
if (!isInIframe) {
status.value = 'error';
error.value = 'not-in-iframe';
return;
}

if (response.ok) {
oauth.value = 'authenticated';
if (!isAuthenticated()) {
sendValidateMessageToParent();
return;
}

sendBeginOAuthMessageToParent(response.redirectTo);
status.value = 'authenticated';
error.value = undefined;

await authenticated();
};

const isAuthenticated = () => {
try {
const payload: DecodedToken = JSON.parse(
sessionStorage.getItem(KEY_VALIDATED_PAYLOAD) || '',
);
return payload && new Date().getTime() / 1000 < payload.exp;
} catch (err) {
return false;
}
};

const sendValidateMessageToParent = () => {
status.value = 'authenticating';
error.value = undefined;
const host = getParentHost();
const slug = getSlug();

try {
const type = appConfig.appBridge.type;
const payload = createValidateMessagePayload({ type, slug });

postMessageToParent(payload);
sessionStorage.setItem(KEY_PARENT_HOST, host);
sessionStorage.setItem(KEY_SLUG, slug || '');
} catch (err) {
sessionStorage.removeItem(KEY_PARENT_HOST);
}
};

const createValidateMessagePayload: CreateValidateMessagePayload = ({
type,
slug,
}) => {
const payload: ValidateMessagePayload = {
action: getPostMessageAction(type),
event: 'validate',
};

if (type === 'tool-plugin') {
payload.tool = slug;
}

return payload;
};

const eventListener = async (event: MessageEvent) => {
Expand All @@ -93,10 +146,7 @@ const useAppBridgeMessages = () => {
);
status.value = 'authenticated';
error.value = undefined;

if (appConfig.appBridge.oauth) {
await startOAuth();
}
await authenticated();
} else {
sessionStorage.removeItem(KEY_TOKEN);
sessionStorage.removeItem(KEY_VALIDATED_PAYLOAD);
Expand All @@ -112,90 +162,52 @@ const useAppBridgeMessages = () => {
}
};

const isAuthenticated = () => {
try {
const payload: DecodedToken = JSON.parse(
sessionStorage.getItem(KEY_VALIDATED_PAYLOAD) || '',
);
return payload && new Date().getTime() / 1000 < payload.exp;
} catch (err) {
return false;
}
};
// Adds event listener to listen to events coming from Storyblok to Iframe (plugin)
onMounted(async () => {
window.addEventListener('message', eventListener);
});

const getParentHost = () => {
const storedHost = sessionStorage.getItem(KEY_PARENT_HOST);
if (storedHost) {
return storedHost;
}
const params = new URLSearchParams(location.search);
const protocol = params.get('protocol');
const host = params.get('host');
if (!protocol || !host) {
throw new Error('Missing `protocol` or `host` in query params');
}
return `${protocol}//${host}`;
};
onUnmounted(() => {
window.removeEventListener('message', eventListener);
});

const getSlug = () => {
const storedSlug = sessionStorage.getItem(KEY_SLUG);
if (storedSlug) {
return storedSlug;
}
const params = new URLSearchParams(location.search);
return params.get('slug');
};
return { status, init, error };
};

//TODO: rename initAppBridge?
const init = async () => {
const isInIframe = window.top !== window.self;
const useOAuth = () => {
const appConfig = useAppConfig();
const status = ref<'disabled' | 'init' | 'authenticating' | 'authenticated'>(
appConfig.appBridge.oauth ? 'init' : 'disabled',
);

if (!isInIframe) {
status.value = 'error';
error.value = 'not-in-iframe';
return;
}
const init = async () => {
status.value = 'authenticating';

if (!isAuthenticated()) {
sendValidateMessageToParent();
return;
}
const initOAuth =
new URLSearchParams(location.search).get('init_oauth') === 'true';

status.value = 'authenticated';
error.value = undefined;
const response = await $fetch('/api/_oauth', {
method: 'POST',
body: { initOAuth },
});

if (appConfig.appBridge.oauth) {
await startOAuth();
if (response.ok) {
status.value = 'authenticated';
return;
}

return;
};

const sendValidateMessageToParent = () => {
status.value = 'authenticating';
error.value = undefined;
const host = getParentHost();
const slug = getSlug();

try {
const type = appConfig.appBridge.type;
const payload = createValidateMessagePayload({ type, slug });

window.parent.postMessage(payload, host);
sessionStorage.setItem(KEY_PARENT_HOST, host);
sessionStorage.setItem(KEY_SLUG, slug || '');
} catch (err) {
sessionStorage.removeItem(KEY_PARENT_HOST);
if (initOAuth) {
sendBeginOAuthMessageToParent(response.redirectTo);
} else {
window.location.href = response.redirectTo;
}
};

const sendBeginOAuthMessageToParent = (redirectTo: string) => {
const host = getParentHost();
const slug = getSlug();
const type = appConfig.appBridge.type;
const payload = createOAuthInitMessagePayload({ type, slug, redirectTo });
window.parent.postMessage(payload, host);
postMessageToParent(payload);
};

const createOAuthInitMessagePayload: CreateBeginOAuthMessagePayload = ({
Expand All @@ -216,52 +228,48 @@ const useAppBridgeMessages = () => {
return payload;
};

const createValidateMessagePayload: CreateValidateMessagePayload = ({
type,
slug,
}) => {
const payload: ValidateMessagePayload = {
action: getPostMessageAction(type),
event: 'validate',
};

if (type === 'tool-plugin') {
payload.tool = slug;
}

return payload;
};

// Adds even listener to listen to events coming from Storyblok to Iframe (plugin)
onMounted(async () => {
window.addEventListener('message', eventListener);
});

onUnmounted(() => {
window.removeEventListener('message', eventListener);
});

return {
status,
oauth,
init,
};
return { init, status };
};

export const useAppBridge = () => {
const nuxtApp = useNuxtApp();
const appConfig = useAppConfig();
const { status, oauth, init } = useAppBridgeMessages();

if (appConfig.appBridge.enabled && nuxtApp.payload.serverRendered) {
throw new Error(
'To use App Bridge, you must configure `ssr: false` in your `nuxt.config.ts` file.',
);
}

const { init: initOAuth, status: oauthStatus } = useOAuth();

const { init: initAppBridgeAuth, status: appBridgeAuthStatus } =
useAppBridgeAuth({
authenticated: async () => {
if (appConfig.appBridge.oauth) {
await initOAuth();
}
},
});

const completed = computed(() => {
if (appConfig.appBridge.oauth) {
return (
appBridgeAuthStatus.value === 'authenticated' &&
oauthStatus.value === 'authenticated'
);
} else {
return appBridgeAuthStatus.value === 'authenticated';
}
});

if (appConfig.appBridge.enabled) {
init();
initAppBridgeAuth();
}

return { status, oauth };
return {
completed,
appBridgeAuth: appBridgeAuthStatus,
oauth: oauthStatus,
};
};
Loading