Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,18 @@
"compat-verdict-compatible-detail": "This bundle can be delivered over-the-air to devices running {bundle}.",
"compat-verdict-incompatible": "Not compatible ({count})",
"compat-verdict-incompatible-detail": "Some packages require an app store update, so this bundle cannot be delivered over-the-air.",
"compat-fix-explanation": "Capgo delivers your web and JavaScript code instantly, but it can't change native code. When a bundle's native dependencies don't match the native build a device already has, that update can crash or misbehave until your users install a new native build from the app stores.",
"compat-fix-how-title": "How to fix it",
"compat-fix-learn-more": "Learn more in the docs",
"compat-fix-manage-channels": "Manage channels",
"compat-fix-rebuild-cta": "Open Capgo Builder",
"compat-fix-rebuild-detail": "Build for iOS and Android in the cloud, then submit to the stores — no local Xcode or Android Studio needed.",
"compat-fix-rebuild-title": "Ship a new native build",
"compat-fix-rollback-detail": "Point the affected channel back to its last compatible bundle so users stay safe until the new build is live.",
"compat-fix-rollback-title": "Roll back the channel",
"compat-fix-title": "A new native build is needed",
"compat-fix-why-detail": "Over-the-air updates can only replace the web layer — JavaScript, HTML and CSS. Native plugins are compiled into the app binary, which only the App Store and Play Store can update. When a bundle's native packages differ from the build installed on a device, its JavaScript can call native APIs that aren't there — which is why it can crash.",
"compat-fix-why-title": "Why native changes need an app-store update",
"dependencies": "Dependencies",
"dependencies-added-packages": "Added",
"dependencies-changed-packages": "Changed",
Expand Down
122 changes: 121 additions & 1 deletion src/pages/app/[app].compatibility.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import { toast } from 'vue-sonner'
import IconAlertCircle from '~icons/lucide/alert-circle'
import IconArrowRight from '~icons/lucide/arrow-right'
import IconCheckCircle from '~icons/lucide/check-circle'
import IconChevronRight from '~icons/lucide/chevron-right'
import IconExternalLink from '~icons/lucide/external-link'
import { dependencyDiffPath, groupCompatibilityEvents, platformLabel } from '~/services/compatibilityEvents'
import { formatLocalDateTime } from '~/services/date'
import { pushEvent } from '~/services/posthog'
import { createSignedImageUrl } from '~/services/storage'
import { useSupabase } from '~/services/supabase'
import { getLocalConfig, useSupabase } from '~/services/supabase'
import { useDialogV2Store } from '~/stores/dialogv2'
import { useDisplayStore } from '~/stores/display'

Expand Down Expand Up @@ -54,6 +56,33 @@ const visibleGroups = computed<CompatibilityEventGroup[]>(() => {
return groupedEvents.value
})

// Whether the app currently has any live incompatibility, which is when the
// "what this means / how to fix it" guidance + Capgo Builder CTA are relevant.
const hasUnresolved = computed(() => groupedEvents.value.some(group => !group.resolved))

const config = getLocalConfig()
// Docs that explain why native changes can't ship over-the-air (the bundle
// compatibility / disable-updates strategy section).
const compatDocsUrl = 'https://capgo.app/docs/cli/commands/#disable-updates-strategy'

// Send the user to the in-app native build flow (Builds tab), tracking the click
// so this Builder entry point can be compared with the banner / upload CTAs.
function openBuilder() {
pushEvent('builder_cta_compatibility_clicked', config.supaHost, { app_id: id.value })
router.push(`/app/${encodeURIComponent(id.value)}/builds`)
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
// The guidance panel is collapsible and remembers the user's choice across
// visits (default expanded; only collapsed if they hid it before).
const guidanceCollapseKey = 'capgo-compat-guidance-collapsed'
const guidanceOpen = ref(typeof localStorage === 'undefined' || localStorage.getItem(guidanceCollapseKey) !== '1')

function toggleGuidance() {
guidanceOpen.value = !guidanceOpen.value
if (typeof localStorage !== 'undefined')
localStorage.setItem(guidanceCollapseKey, guidanceOpen.value ? '0' : '1')
}

async function loadAppInfo() {
try {
const { data: dataApp } = await supabase
Expand Down Expand Up @@ -349,6 +378,97 @@ watchEffect(async () => {
</label>
</div>

<!-- Fix guidance + Capgo Builder CTA, shown while the app has live incompatibilities -->
<section
v-if="hasUnresolved"
data-test="compatibility-fix-guidance"
class="overflow-hidden border rounded-xl border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900"
>
<button
type="button"
data-test="compatibility-fix-guidance-toggle"
class="flex items-center w-full gap-3 px-5 py-4 text-left transition-colors hover:bg-slate-50 dark:hover:bg-slate-800/40"
:aria-expanded="guidanceOpen"
@click="toggleGuidance"
>
<IconAlertCircle class="w-5 h-5 shrink-0 text-amber-500 dark:text-amber-400" />
<span class="flex-1 text-base font-semibold text-slate-900 dark:text-white">
{{ t('compat-fix-title') }}
</span>
<IconChevronRight
class="w-4 h-4 transition-transform shrink-0 text-slate-400"
:class="guidanceOpen ? 'rotate-90' : ''"
/>
</button>

<div v-show="guidanceOpen" class="px-5 pb-5 border-t border-slate-100 dark:border-slate-800">
<p class="pt-4 text-sm leading-relaxed text-slate-600 dark:text-slate-300">
{{ t('compat-fix-explanation') }}
</p>

<h3 class="mt-4 text-xs font-semibold tracking-wide uppercase text-slate-400 dark:text-slate-500">
{{ t('compat-fix-how-title') }}
</h3>
<div class="grid gap-3 mt-3 sm:grid-cols-2">
<div class="flex flex-col p-4 border rounded-lg border-slate-200 dark:border-slate-700">
<h4 class="text-sm font-semibold text-slate-900 dark:text-white">
{{ t('compat-fix-rebuild-title') }}
</h4>
<p class="flex-1 mt-1 text-sm leading-relaxed text-slate-600 dark:text-slate-400">
{{ t('compat-fix-rebuild-detail') }}
</p>
<button
type="button"
data-test="compatibility-rebuild-cta"
class="inline-flex items-center self-start gap-1.5 px-4 py-2 mt-4 text-sm font-semibold text-white transition-colors rounded-lg bg-blue-600 hover:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 dark:focus:ring-offset-slate-900"
@click="openBuilder"
>
{{ t('compat-fix-rebuild-cta') }}
<IconArrowRight class="w-4 h-4" />
</button>
</div>
<div class="flex flex-col p-4 border rounded-lg border-slate-200 dark:border-slate-700">
<h4 class="text-sm font-semibold text-slate-900 dark:text-white">
{{ t('compat-fix-rollback-title') }}
</h4>
<p class="flex-1 mt-1 text-sm leading-relaxed text-slate-600 dark:text-slate-400">
{{ t('compat-fix-rollback-detail') }}
</p>
<button
type="button"
class="inline-flex items-center self-start gap-1.5 px-4 py-2 mt-4 text-sm font-semibold transition-colors border rounded-lg border-slate-300 text-slate-700 hover:bg-slate-50 dark:border-slate-600 dark:text-slate-200 dark:hover:bg-slate-800"
@click="router.push(`/app/${encodeURIComponent(id)}/channels`)"
>
{{ t('compat-fix-manage-channels') }}
<IconArrowRight class="w-4 h-4" />
</button>
</div>
</div>

<div class="pt-4 mt-4 border-t border-slate-100 dark:border-slate-800">
<details class="group">
<summary class="inline-flex items-center gap-1 text-sm font-medium cursor-pointer list-none text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200">
<IconChevronRight class="w-4 h-4 transition-transform group-open:rotate-90" />
{{ t('compat-fix-why-title') }}
</summary>
<p class="mt-2 pl-5 text-sm leading-relaxed text-slate-600 dark:text-slate-300">
{{ t('compat-fix-why-detail') }}
</p>
</details>
<a
:href="compatDocsUrl"
target="_blank"
rel="noopener noreferrer"
data-test="compatibility-docs-link"
class="inline-flex items-center gap-1 mt-3 text-sm font-medium text-blue-600 hover:underline dark:text-blue-400"
>
{{ t('compat-fix-learn-more') }}
<IconExternalLink class="h-3.5 w-3.5" />
</a>
</div>
</div>
</section>

<!-- Empty state -->
<div
v-if="!isLoading && visibleGroups.length === 0"
Expand Down
Loading