From 184914ba4f62d4d2266d05ae9f4a8c607fe227d1 Mon Sep 17 00:00:00 2001 From: Akash K <57758277+amk-dev@users.noreply.github.com> Date: Thu, 19 May 2022 13:41:05 +0530 Subject: [PATCH] feat: extension identification improvements (#2332) Co-authored-by: Andrew Bastin --- .../components/app/Interceptor.vue | 87 +++++++------------ .../hoppscotch-app/components/smart/Radio.vue | 42 +++++---- .../components/smart/RadioGroup.vue | 12 ++- .../helpers/strategies/ExtensionStrategy.ts | 62 ++++++++++--- .../ExtensionStrategy-NoProxy.spec.js | 7 -- packages/hoppscotch-app/layouts/default.vue | 66 ++++++++++++++ .../hoppscotch-app/newstore/HoppExtension.ts | 40 +++++++++ packages/hoppscotch-app/pages/settings.vue | 40 +++------ .../hoppscotch-app/types/pw-ext-hook.d.ts | 9 ++ packages/hoppscotch-app/types/window.d.ts | 7 +- 10 files changed, 243 insertions(+), 129 deletions(-) create mode 100644 packages/hoppscotch-app/newstore/HoppExtension.ts diff --git a/packages/hoppscotch-app/components/app/Interceptor.vue b/packages/hoppscotch-app/components/app/Interceptor.vue index 6a993a37b..f5b5e15ea 100644 --- a/packages/hoppscotch-app/components/app/Interceptor.vue +++ b/packages/hoppscotch-app/components/app/Interceptor.vue @@ -8,11 +8,7 @@ {{ t("settings.interceptor_description") }}

- +
diff --git a/packages/hoppscotch-app/components/smart/Radio.vue b/packages/hoppscotch-app/components/smart/Radio.vue index 226904dc4..de1ced5f3 100644 --- a/packages/hoppscotch-app/components/smart/Radio.vue +++ b/packages/hoppscotch-app/components/smart/Radio.vue @@ -1,33 +1,31 @@ - diff --git a/packages/hoppscotch-app/components/smart/RadioGroup.vue b/packages/hoppscotch-app/components/smart/RadioGroup.vue index f0363078b..1b1027819 100644 --- a/packages/hoppscotch-app/components/smart/RadioGroup.vue +++ b/packages/hoppscotch-app/components/smart/RadioGroup.vue @@ -5,18 +5,22 @@ :key="`radio-${index}`" :value="radio.value" :label="radio.label" - :selected="selected" - @change="$emit('change', radio.value)" + :selected="value === radio.value" + @change="emit('input', radio.value)" />
diff --git a/packages/hoppscotch-app/helpers/strategies/ExtensionStrategy.ts b/packages/hoppscotch-app/helpers/strategies/ExtensionStrategy.ts index 013155f04..254259f12 100644 --- a/packages/hoppscotch-app/helpers/strategies/ExtensionStrategy.ts +++ b/packages/hoppscotch-app/helpers/strategies/ExtensionStrategy.ts @@ -1,4 +1,5 @@ import * as TE from "fp-ts/TaskEither" +import * as O from "fp-ts/Option" import { pipe } from "fp-ts/function" import { AxiosRequestConfig } from "axios" import cloneDeep from "lodash/cloneDeep" @@ -15,12 +16,42 @@ export const hasFirefoxExtensionInstalled = () => hasExtensionInstalled() && browserIsFirefox() export const cancelRunningExtensionRequest = () => { - if ( - hasExtensionInstalled() && - window.__POSTWOMAN_EXTENSION_HOOK__.cancelRunningRequest - ) { - window.__POSTWOMAN_EXTENSION_HOOK__.cancelRunningRequest() + window.__POSTWOMAN_EXTENSION_HOOK__?.cancelRunningRequest() +} + +export const defineSubscribableObject = (obj: T) => { + const proxyObject = { + ...obj, + _subscribers: {} as { + // eslint-disable-next-line no-unused-vars + [key in keyof T]?: ((...args: any[]) => any)[] + }, + subscribe(prop: keyof T, func: (...args: any[]) => any): void { + if (Array.isArray(this._subscribers[prop])) { + this._subscribers[prop]?.push(func) + } else { + this._subscribers[prop] = [func] + } + }, } + + type SubscribableProxyObject = typeof proxyObject + + return new Proxy(proxyObject, { + set(obj, prop, newVal) { + obj[prop as keyof SubscribableProxyObject] = newVal + + const currentSubscribers = obj._subscribers[prop as keyof T] + + if (Array.isArray(currentSubscribers)) { + for (const subscriber of currentSubscribers) { + subscriber(newVal) + } + } + + return true + }, + }) } const preProcessRequest = (req: AxiosRequestConfig): AxiosRequestConfig => { @@ -56,13 +87,20 @@ const extensionStrategy: NetworkStrategy = (req) => // Run the request TE.bind("response", ({ processedReq }) => - TE.tryCatch( - () => - window.__POSTWOMAN_EXTENSION_HOOK__.sendRequest({ - ...processedReq, - wantsBinary: true, - }) as Promise, - (err) => err as any + pipe( + window.__POSTWOMAN_EXTENSION_HOOK__, + O.fromNullable, + TE.fromOption(() => "NO_PW_EXT_HOOK" as const), + TE.chain((extensionHook) => + TE.tryCatch( + () => + extensionHook.sendRequest({ + ...processedReq, + wantsBinary: true, + }), + (err) => err as any + ) + ) ) ), diff --git a/packages/hoppscotch-app/helpers/strategies/__tests__/ExtensionStrategy-NoProxy.spec.js b/packages/hoppscotch-app/helpers/strategies/__tests__/ExtensionStrategy-NoProxy.spec.js index b44a6f9af..53665d492 100644 --- a/packages/hoppscotch-app/helpers/strategies/__tests__/ExtensionStrategy-NoProxy.spec.js +++ b/packages/hoppscotch-app/helpers/strategies/__tests__/ExtensionStrategy-NoProxy.spec.js @@ -122,13 +122,6 @@ describe("cancelRunningExtensionRequest", () => { cancelRunningExtensionRequest() expect(cancelFunc).not.toHaveBeenCalled() }) - - test("does not cancel request if extension installed but function not present", () => { - global.__POSTWOMAN_EXTENSION_HOOK__ = {} - - cancelRunningExtensionRequest() - expect(cancelFunc).not.toHaveBeenCalled() - }) }) describe("extensionStrategy", () => { diff --git a/packages/hoppscotch-app/layouts/default.vue b/packages/hoppscotch-app/layouts/default.vue index cf40259f3..971673e00 100644 --- a/packages/hoppscotch-app/layouts/default.vue +++ b/packages/hoppscotch-app/layouts/default.vue @@ -64,6 +64,8 @@ import { useRouter, watch, ref, + onMounted, + onBeforeUnmount, } from "@nuxtjs/composition-api" import { Splitpanes, Pane } from "splitpanes" import "splitpanes/dist/splitpanes.css" @@ -77,6 +79,12 @@ import { hookKeybindingsListener } from "~/helpers/keybindings" import { defineActionHandler } from "~/helpers/actions" import { useSentry } from "~/helpers/sentry" import { useColorMode } from "~/helpers/utils/composables" +import { + changeExtensionStatus, + ExtensionStatus, +} from "~/newstore/HoppExtension" + +import { defineSubscribableObject } from "~/helpers/strategies/ExtensionStrategy" function appLayout() { const rightSidebar = useSetting("SIDEBAR") @@ -202,6 +210,62 @@ function defineJumpActions() { }) } +function setupExtensionHooks() { + const extensionPollIntervalId = ref>() + + onMounted(() => { + if (window.__HOPP_EXTENSION_STATUS_PROXY__) { + changeExtensionStatus(window.__HOPP_EXTENSION_STATUS_PROXY__.status) + + window.__HOPP_EXTENSION_STATUS_PROXY__.subscribe( + "status", + (status: ExtensionStatus) => changeExtensionStatus(status) + ) + } else { + const statusProxy = defineSubscribableObject({ + status: "waiting" as ExtensionStatus, + }) + + window.__HOPP_EXTENSION_STATUS_PROXY__ = statusProxy + statusProxy.subscribe("status", (status: ExtensionStatus) => + changeExtensionStatus(status) + ) + + /** + * Keeping identifying extension backward compatible + * We are assuming the default version is 0.24 or later. So if the extension exists, its identified immediately, + * then we use a poll to find the version, this will get the version for 0.24 and any other version + * of the extension, but will have a slight lag. + * 0.24 users will get the benefits of 0.24, while the extension won't break for the old users + */ + extensionPollIntervalId.value = setInterval(() => { + if (typeof window.__POSTWOMAN_EXTENSION_HOOK__ !== "undefined") { + if (extensionPollIntervalId.value) + clearInterval(extensionPollIntervalId.value) + + const version = window.__POSTWOMAN_EXTENSION_HOOK__.getVersion() + + // When the version is not 0.24 or higher, the extension wont do this. so we have to do it manually + if ( + version.major === 0 && + version.minor <= 23 && + window.__HOPP_EXTENSION_STATUS_PROXY__ + ) { + window.__HOPP_EXTENSION_STATUS_PROXY__.status = "available" + } + } + }, 2000) + } + }) + + // Cleanup timer + onBeforeUnmount(() => { + if (extensionPollIntervalId.value) { + clearInterval(extensionPollIntervalId.value) + } + }) +} + export default defineComponent({ components: { Splitpanes, Pane }, setup() { @@ -229,6 +293,8 @@ export default defineComponent({ showSupport.value = !showSupport.value }) + setupExtensionHooks() + return { mdAndLarger, spacerClass, diff --git a/packages/hoppscotch-app/newstore/HoppExtension.ts b/packages/hoppscotch-app/newstore/HoppExtension.ts new file mode 100644 index 000000000..ae725333f --- /dev/null +++ b/packages/hoppscotch-app/newstore/HoppExtension.ts @@ -0,0 +1,40 @@ +import { distinctUntilChanged, pluck } from "rxjs" +import DispatchingStore, { defineDispatchers } from "./DispatchingStore" + +export type ExtensionStatus = "available" | "unknown-origin" | "waiting" + +type InitialState = { + extensionStatus: ExtensionStatus +} + +const initialState: InitialState = { + extensionStatus: "waiting", +} + +const dispatchers = defineDispatchers({ + changeExtensionStatus( + _, + { extensionStatus }: { extensionStatus: ExtensionStatus } + ) { + return { + extensionStatus, + } + }, +}) + +export const hoppExtensionStore = new DispatchingStore( + initialState, + dispatchers +) + +export const extensionStatus$ = hoppExtensionStore.subject$.pipe( + pluck("extensionStatus"), + distinctUntilChanged() +) + +export function changeExtensionStatus(extensionStatus: ExtensionStatus) { + hoppExtensionStore.dispatch({ + dispatcher: "changeExtensionStatus", + payload: { extensionStatus }, + }) +} diff --git a/packages/hoppscotch-app/pages/settings.vue b/packages/hoppscotch-app/pages/settings.vue index da460ad2b..78f9df3c3 100644 --- a/packages/hoppscotch-app/pages/settings.vue +++ b/packages/hoppscotch-app/pages/settings.vue @@ -241,14 +241,11 @@ import { useToast, useI18n, useColorMode, - usePolled, + useReadonlyStream, } from "~/helpers/utils/composables" -import { - hasExtensionInstalled, - hasChromeExtensionInstalled, - hasFirefoxExtensionInstalled, -} from "~/helpers/strategies/ExtensionStrategy" + import { browserIsChrome, browserIsFirefox } from "~/helpers/utils/userAgent" +import { extensionStatus$ } from "~/newstore/HoppExtension" const t = useI18n() const toast = useToast() @@ -263,30 +260,21 @@ const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION") const SIDEBAR_ON_LEFT = useSetting("SIDEBAR_ON_LEFT") const ZEN_MODE = useSetting("ZEN_MODE") -const extensionVersion = usePolled(5000, (stopPolling) => { - const result = hasExtensionInstalled() - ? window.__POSTWOMAN_EXTENSION_HOOK__.getVersion() +const currentExtensionStatus = useReadonlyStream(extensionStatus$, null) + +const extensionVersion = computed(() => { + return currentExtensionStatus.value === "available" + ? window.__POSTWOMAN_EXTENSION_HOOK__?.getVersion() ?? null : null - - // We don't need to poll anymore after we get value - if (result) stopPolling() - - return result }) -const hasChromeExtInstalled = usePolled(5000, (stopPolling) => { - // If not Chrome, we don't need to worry about this value changing - if (!browserIsChrome()) stopPolling() +const hasChromeExtInstalled = computed( + () => browserIsChrome() && currentExtensionStatus.value === "available" +) - return hasChromeExtensionInstalled() -}) - -const hasFirefoxExtInstalled = usePolled(5000, (stopPolling) => { - // If not Chrome, we don't need to worry about this value changing - if (!browserIsFirefox()) stopPolling() - - return hasFirefoxExtensionInstalled() -}) +const hasFirefoxExtInstalled = computed( + () => browserIsFirefox() && currentExtensionStatus.value === "available" +) const clearIcon = ref("rotate-ccw") diff --git a/packages/hoppscotch-app/types/pw-ext-hook.d.ts b/packages/hoppscotch-app/types/pw-ext-hook.d.ts index bdc172c38..8e39ff1e2 100644 --- a/packages/hoppscotch-app/types/pw-ext-hook.d.ts +++ b/packages/hoppscotch-app/types/pw-ext-hook.d.ts @@ -1,5 +1,6 @@ import { AxiosRequestConfig } from "axios" import { NetworkResponse } from "~/helpers/network" +import { ExtensionStatus } from "~/newstore/HoppExtension" export interface PWExtensionHook { getVersion: () => { major: number; minor: number } @@ -8,3 +9,11 @@ export interface PWExtensionHook { ) => Promise cancelRunningRequest: () => void } + +export type HoppExtensionStatusHook = { + status: ExtensionStatus + _subscribers: { + status?: ((...args: any[]) => any)[] | undefined + } + subscribe(prop: "status", func: (...args: any[]) => any): void +} diff --git a/packages/hoppscotch-app/types/window.d.ts b/packages/hoppscotch-app/types/window.d.ts index 81a6a9d02..e37081260 100644 --- a/packages/hoppscotch-app/types/window.d.ts +++ b/packages/hoppscotch-app/types/window.d.ts @@ -1,9 +1,8 @@ -import { PWExtensionHook } from "./pw-ext-hook" - -export {} +import { HoppExtensionStatusHook, PWExtensionHook } from "./pw-ext-hook" declare global { interface Window { - __POSTWOMAN_EXTENSION_HOOK__: PWExtensionHook + __POSTWOMAN_EXTENSION_HOOK__: PWExtensionHook | undefined + __HOPP_EXTENSION_STATUS_PROXY__: HoppExtensionStatusHook | undefined } }