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
}
}