From 36d71fc12703088cdc0b127ba3e3784599d2f2d7 Mon Sep 17 00:00:00 2001 From: Shreyas Date: Tue, 4 Mar 2025 16:58:33 +0530 Subject: [PATCH] fix(common): extension port to relay regression (#4806) --- .../src/components/settings/Extension.vue | 11 ++- .../kernel-interceptors/extension/index.ts | 54 ++++++++++++-- .../kernel-interceptors/extension/store.ts | 73 +++++++++++++++++++ 3 files changed, 130 insertions(+), 8 deletions(-) diff --git a/packages/hoppscotch-common/src/components/settings/Extension.vue b/packages/hoppscotch-common/src/components/settings/Extension.vue index 742eea26..7d3bc91e 100644 --- a/packages/hoppscotch-common/src/components/settings/Extension.vue +++ b/packages/hoppscotch-common/src/components/settings/Extension.vue @@ -39,14 +39,19 @@ import IconChrome from "~icons/brands/chrome" import IconFirefox from "~icons/brands/firefox" import IconCheckCircle from "~icons/lucide/check-circle" import { useI18n } from "@composables/i18n" -import { ExtensionInterceptorService } from "~/platform/std/interceptors/extension" +import { computed } from "vue" import { useService } from "dioc/vue" +import { ExtensionKernelInterceptorService } from "~/platform/std/kernel-interceptors/extension" const t = useI18n() -const extensionService = useService(ExtensionInterceptorService) +const extensionService = useService(ExtensionKernelInterceptorService) + +const extensionVersion = computed(() => { + const versionOption = extensionService.extensionVersion + return versionOption.value ? versionOption.value : null +}) -const extensionVersion = extensionService.extensionVersion const hasChromeExtInstalled = extensionService.chromeExtensionInstalled const hasFirefoxExtInstalled = extensionService.firefoxExtensionInstalled diff --git a/packages/hoppscotch-common/src/platform/std/kernel-interceptors/extension/index.ts b/packages/hoppscotch-common/src/platform/std/kernel-interceptors/extension/index.ts index 9767ad65..8ed97d53 100644 --- a/packages/hoppscotch-common/src/platform/std/kernel-interceptors/extension/index.ts +++ b/packages/hoppscotch-common/src/platform/std/kernel-interceptors/extension/index.ts @@ -1,4 +1,4 @@ -import { computed, markRaw } from "vue" +import { computed, markRaw, ref } from "vue" import { Service } from "dioc" import type { RelayRequest, RelayResponse } from "@hoppscotch/kernel" import { body } from "@hoppscotch/kernel" @@ -14,6 +14,11 @@ import * as E from "fp-ts/Either" import { getI18n } from "~/modules/i18n" import { until } from "@vueuse/core" import { preProcessRelayRequest } from "~/helpers/functional/preprocess" +import { browserIsChrome, browserIsFirefox } from "~/helpers/utils/userAgent" + +export const cancelRunningExtensionRequest = async () => { + window.__POSTWOMAN_EXTENSION_HOOK__?.cancelRequest() +} export class ExtensionKernelInterceptorService extends Service @@ -23,8 +28,15 @@ export class ExtensionKernelInterceptorService private readonly store = this.bind(KernelInterceptorExtensionStore) public readonly id = "extension" - public readonly name = (t: ReturnType) => - t("interceptor.extension.name") + public readonly name = (t: ReturnType) => { + const version = this.extensionVersion.value + + if (this.extensionStatus.value === "available" && version) { + return `${t("settings.extensions")}: v${version.major}.${version.minor}` + } + return `${t("settings.extensions")}: ${t("settings.extension_ver_not_reported")}` + } + public readonly selectable = { type: "selectable" as const } public readonly capabilities = { method: new Set([ @@ -60,19 +72,34 @@ export class ExtensionKernelInterceptorService } as const public readonly settingsEntry = markRaw({ - title: (t: ReturnType) => - t("interceptor.extension.settings_title"), + title: (t: ReturnType) => t("settings.extensions"), component: SettingsExtension, }) public readonly subtitle = markRaw(SettingsExtensionSubtitle) + public readonly extensionStatus = computed( () => this.store.getSettings().status ) + public readonly extensionVersion = computed( () => this.store.getSettings().extensionVersion ) + /** + * Whether the extension is installed in Chrome or not. + */ + public readonly chromeExtensionInstalled = computed( + () => this.extensionStatus.value === "available" && browserIsChrome() + ) + + /** + * Whether the extension is installed in Firefox or not. + */ + public readonly firefoxExtensionInstalled = computed( + () => this.extensionStatus.value === "available" && browserIsFirefox() + ) + private async executeExtensionRequest( request: RelayRequest ): Promise> { @@ -111,6 +138,23 @@ export class ExtensionKernelInterceptorService ), }) } catch (e) { + console.error(e) + + if (e instanceof Error && "response" in e) { + const response = (e as any).response + if (response) { + return E.right({ + status: response.status, + statusText: response.statusText, + headers: response.headers, + body: body.body( + response.data, + response.headers["content-type"] + ), + }) + } + } + return E.left({ humanMessage: { heading: (t) => t("error.extension.heading"), diff --git a/packages/hoppscotch-common/src/platform/std/kernel-interceptors/extension/store.ts b/packages/hoppscotch-common/src/platform/std/kernel-interceptors/extension/store.ts index a127d558..a2a62e0c 100644 --- a/packages/hoppscotch-common/src/platform/std/kernel-interceptors/extension/store.ts +++ b/packages/hoppscotch-common/src/platform/std/kernel-interceptors/extension/store.ts @@ -2,6 +2,7 @@ import { Service } from "dioc" import { Store } from "~/kernel/store" import * as E from "fp-ts/Either" import * as O from "fp-ts/Option" +import { ref } from "vue" const STORE_NAMESPACE = "interceptors.extension.v1" const SETTINGS_KEY = "settings" @@ -13,6 +14,41 @@ export type ExtensionVersion = { minor: number } +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 + }, + }) +} + type ExtensionSettings = { version: "v1" status: ExtensionStatus @@ -34,6 +70,7 @@ export class KernelInterceptorExtensionStore extends Service { public static readonly ID = "KERNEL_EXTENSION_INTERCEPTOR_STORE" private settings: ExtensionSettings = { ...DEFAULT_SETTINGS } + private extensionPollIntervalId = ref>() async onServiceInit(): Promise { const initResult = await Store.init() @@ -76,6 +113,42 @@ export class KernelInterceptorExtensionStore extends Service { this.updateSettings({ status }) } ) + } else { + const statusProxy = defineSubscribableObject({ + status: "waiting" as ExtensionStatus, + }) + + window.__HOPP_EXTENSION_STATUS_PROXY__ = statusProxy + statusProxy.subscribe( + "status", + (status: ExtensionStatus) => this.updateSettings({ 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 + */ + this.extensionPollIntervalId.value = setInterval(() => { + if (typeof window.__POSTWOMAN_EXTENSION_HOOK__ !== "undefined") { + if (this.extensionPollIntervalId.value) + clearInterval(this.extensionPollIntervalId.value) + + const version = window.__POSTWOMAN_EXTENSION_HOOK__.getVersion() + this.updateSettings({ extensionVersion: version }) + + // 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) } }