diff --git a/frontend/projects/shared/src/util/misc.util.ts b/frontend/projects/shared/src/util/misc.util.ts index 85dc6231f..7123a95cc 100644 --- a/frontend/projects/shared/src/util/misc.util.ts +++ b/frontend/projects/shared/src/util/misc.util.ts @@ -36,3 +36,16 @@ export function debounce(delay: number = 300): MethodDecorator { return descriptor } } + +export function removeTrailingSlash(word: string): string { + return word.replace(/\/+$/, '') +} + +export function isValidHttpUrl(string: string): boolean { + try { + const _ = new URL(string) + return true + } catch (_) { + return false + } +} diff --git a/frontend/projects/shared/styles/shared.scss b/frontend/projects/shared/styles/shared.scss index e794788c3..b6e0fcf77 100644 --- a/frontend/projects/shared/styles/shared.scss +++ b/frontend/projects/shared/styles/shared.scss @@ -110,4 +110,12 @@ ion-modal::part(content) { .montserrat { font-family: 'Montserrat', sans-serif !important; +} + +.color-success-shade { + color: var(--ion-color-success-shade) +} + +.color-primary-shade { + color: var(--ion-color-primary-shade) } \ No newline at end of file diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html b/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html index deee7307d..b33f3ef1f 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html @@ -24,7 +24,9 @@ [dependencies]="dependencies" > - + diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts index dc69a06f0..93e2d2a1d 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts @@ -1,10 +1,11 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core' +import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' import { NavController } from '@ionic/angular' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { PackageDataEntry, PackageMainStatus, PackageState, + UIMarketplaceData, } from 'src/app/services/patch-db/data-model' import { PackageStatus, @@ -17,6 +18,12 @@ import { import { map, startWith, filter } from 'rxjs/operators' import { ActivatedRoute } from '@angular/router' import { getPkgId } from '@start9labs/shared' +import { MarketplaceService } from 'src/app/services/marketplace.service' +import { + AbstractMarketplaceService, + Marketplace, +} from '@start9labs/marketplace' +import { Observable } from 'rxjs' const STATES = [ PackageState.Installing, @@ -53,6 +60,12 @@ export class AppShowPage { ), ) + readonly currentMarketplace$: Observable = + this.marketplaceService.getMarketplace() + + readonly altMarketplaceData$: Observable = + this.marketplaceService.getAltMarketplace() + readonly connectionFailure$ = this.connectionService .watchFailure$() .pipe(map(failure => failure !== ConnectionFailure.None)) @@ -62,7 +75,9 @@ export class AppShowPage { private readonly navCtrl: NavController, private readonly patch: PatchDbService, private readonly connectionService: ConnectionService, - ) { } + @Inject(AbstractMarketplaceService) + private readonly marketplaceService: MarketplaceService, + ) {} isInstalled( { state }: PackageDataEntry, diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts index 6080fd7a0..37dc1047a 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts @@ -2,12 +2,19 @@ import { Inject, Pipe, PipeTransform } from '@angular/core' import { ActivatedRoute } from '@angular/router' import { DOCUMENT } from '@angular/common' import { AlertController, ModalController, NavController } from '@ionic/angular' -import { MarkdownComponent } from '@start9labs/shared' -import { PackageDataEntry } from 'src/app/services/patch-db/data-model' +import { + isValidHttpUrl, + MarkdownComponent, + removeTrailingSlash, +} from '@start9labs/shared' +import { + PackageDataEntry, + UIMarketplaceData, +} from 'src/app/services/patch-db/data-model' import { ModalService } from 'src/app/services/modal.service' import { ApiService } from 'src/app/services/api/embassy-api.service' import { from } from 'rxjs' - +import { Marketplace } from '@start9labs/marketplace' export interface Button { title: string description: string @@ -30,7 +37,11 @@ export class ToButtonsPipe implements PipeTransform { private readonly apiService: ApiService, ) {} - transform(pkg: PackageDataEntry): Button[] { + transform( + pkg: PackageDataEntry, + currentMarketplace: Marketplace | null, + altMarketplaces: UIMarketplaceData | null | undefined, + ): Button[] { const pkgTitle = pkg.manifest.title return [ @@ -87,7 +98,7 @@ export class ToButtonsPipe implements PipeTransform { icon: 'receipt-outline', }, // view in marketplace - this.viewInMarketplaceButton(pkg), + this.viewInMarketplaceButton(pkg, currentMarketplace, altMarketplaces), // donate { action: () => this.donate(pkg), @@ -112,23 +123,53 @@ export class ToButtonsPipe implements PipeTransform { await modal.present() } - private viewInMarketplaceButton(pkg: PackageDataEntry): Button { - return pkg.installed?.['marketplace-url'] - ? { - action: () => - this.navCtrl.navigateForward([`marketplace/${pkg.manifest.id}`]), - title: 'Marketplace', - description: 'View service in marketplace', - icon: 'storefront-outline', - } - : { - disabled: true, - action: () => {}, - title: 'Marketplace', - description: - 'This package has been side-loaded and is not available in the Start9 Marketplace', - icon: 'storefront-outline', + private viewInMarketplaceButton( + pkg: PackageDataEntry, + currentMarketplace: Marketplace | null, + altMarketplaces: UIMarketplaceData | null | undefined, + ): Button { + const pkgMarketplace = pkg.installed?.['marketplace-url'] + // default button if package marketplace and current marketplace are the same + let button: Button = { + title: 'Marketplace', + icon: 'storefront-outline', + action: () => + this.navCtrl.navigateForward([`marketplace/${pkg.manifest.id}`]), + disabled: false, + description: 'View service in marketplace', + } + if (!pkgMarketplace) { + button.disabled = true + button.description = 'This package was not installed from a marketplace.' + button.action = () => {} + } else if ( + pkgMarketplace && + currentMarketplace && + removeTrailingSlash(pkgMarketplace) !== + removeTrailingSlash(currentMarketplace.url) + ) { + // attempt to get name for pkg marketplace + let pkgTitle = removeTrailingSlash(pkgMarketplace) + if (altMarketplaces) { + const nameOptions = Object.values( + altMarketplaces['known-hosts'], + ).filter(m => m.url === pkgTitle) + if (nameOptions.length) { + // if multiple of the same url exist, they will have the same name, so fine to grab first + pkgTitle = nameOptions[0].name } + } + let marketplaceTitle = removeTrailingSlash(currentMarketplace.url) + // if we found a name for the pkg marketplace, use the name of the currently connected marketplace + if (!isValidHttpUrl(pkgTitle)) { + marketplaceTitle = currentMarketplace.name + } + + button.action = () => + this.differentMarketplaceAction(pkgTitle, marketplaceTitle) + button.description = 'Service was installed from a different marketplace' + } + return button } private async donate({ manifest }: PackageDataEntry): Promise { @@ -143,4 +184,28 @@ export class ToButtonsPipe implements PipeTransform { await alert.present() } } + private async differentMarketplaceAction(pkgM: string, currentM: string) { + const alert = await this.alertCtrl.create({ + header: 'Marketplace Conflict', + message: `This service was installed from: +

+ ${pkgM} +

but you are currently connected to:

+ ${currentM} +

+ To view the marketplace listing for this service, visit your Marketplace Settings and change marketplaces.`, + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, + { + text: 'Go to Settings', + handler: () => this.navCtrl.navigateForward(['embassy/marketplaces']), + cssClass: 'enter-click', + }, + ], + }) + await alert.present() + } } diff --git a/frontend/projects/ui/src/app/services/marketplace.service.ts b/frontend/projects/ui/src/app/services/marketplace.service.ts index 820d6076f..89a65eb3f 100644 --- a/frontend/projects/ui/src/app/services/marketplace.service.ts +++ b/frontend/projects/ui/src/app/services/marketplace.service.ts @@ -92,6 +92,10 @@ export class MarketplaceService extends AbstractMarketplaceService { return this.marketplace$ } + getAltMarketplace(): Observable { + return this.altMarketplaceData$ + } + getCategories(): Observable { return this.categories$ } diff --git a/frontend/projects/ui/src/app/util/web.util.ts b/frontend/projects/ui/src/app/util/web.util.ts index 484c443e9..01f808df6 100644 --- a/frontend/projects/ui/src/app/util/web.util.ts +++ b/frontend/projects/ui/src/app/util/web.util.ts @@ -1,6 +1,7 @@ -export async function copyToClipboard (str: string): Promise { +export async function copyToClipboard(str: string): Promise { if (window.isSecureContext) { - return navigator.clipboard.writeText(str) + return navigator.clipboard + .writeText(str) .then(() => { return true })