-
-
Notifications
You must be signed in to change notification settings - Fork 129
feat(onboarding): track store release step #2413
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,218 @@ | ||
| <script setup lang="ts"> | ||
| import { computed, ref, watch } from 'vue' | ||
| import { useI18n } from 'vue-i18n' | ||
| import { useRouter } from 'vue-router' | ||
| import IconCheck from '~icons/lucide/check' | ||
| import IconCircleDot from '~icons/lucide/circle-dot' | ||
| import IconExternalLink from '~icons/lucide/external-link' | ||
| import IconSmartphone from '~icons/lucide/smartphone' | ||
| import IconStore from '~icons/lucide/store' | ||
| import { defaultApiHost, useSupabase } from '~/services/supabase' | ||
| import { useOrganizationStore } from '~/stores/organization' | ||
|
|
||
| const props = defineProps<{ | ||
| appId: string | ||
| }>() | ||
|
|
||
| const RELEASE_INSTALL_SOURCES = ['app_store'] | ||
| const TEST_INSTALL_SOURCES = ['testflight'] | ||
| const TRACK_UNKNOWN_INSTALL_SOURCES = ['google_play', 'amazon_appstore', 'samsung_galaxy_store', 'huawei_appgallery'] | ||
|
|
||
| const { t } = useI18n() | ||
| const router = useRouter() | ||
| const supabase = useSupabase() | ||
| const organizationStore = useOrganizationStore() | ||
|
|
||
| const isLoading = ref(true) | ||
| const hasLiveUpdateBundle = ref(false) | ||
| const hasStoreInstalledDevice = ref(false) | ||
| const hasTestFlightDevice = ref(false) | ||
| const hasTrackUnknownDevice = ref(false) | ||
| let loadStatusRequestId = 0 | ||
|
|
||
| function resetStatus() { | ||
| hasLiveUpdateBundle.value = false | ||
| hasStoreInstalledDevice.value = false | ||
| hasTestFlightDevice.value = false | ||
| hasTrackUnknownDevice.value = false | ||
| } | ||
|
|
||
| async function countDevicesByInstallSource(appId: string, installSources: string[]) { | ||
| const { data: currentSession } = await supabase.auth.getSession()! | ||
| const currentJwt = currentSession.session?.access_token | ||
| if (!currentJwt) | ||
| return 0 | ||
|
|
||
| const response = await fetch(`${defaultApiHost}/private/devices`, { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| 'authorization': `Bearer ${currentJwt}`, | ||
| }, | ||
| body: JSON.stringify({ | ||
| count: true, | ||
| appId, | ||
| installSources, | ||
| }), | ||
| }) | ||
|
|
||
| if (!response.ok) | ||
| throw new Error(`Cannot count devices by install source: ${response.status}`) | ||
|
|
||
| const data = await response.json() as { count: number } | ||
| return data.count | ||
| } | ||
|
|
||
| const shouldShowBanner = computed(() => { | ||
| return !isLoading.value && hasLiveUpdateBundle.value && !hasStoreInstalledDevice.value | ||
| }) | ||
|
|
||
| const title = computed(() => { | ||
| return hasTrackUnknownDevice.value | ||
| ? t('store-release-onboarding-title-track-unknown') | ||
| : hasTestFlightDevice.value | ||
| ? t('store-release-onboarding-title-testflight') | ||
| : t('store-release-onboarding-title') | ||
|
Check warning on line 75 in src/components/dashboard/StoreReleaseOnboardingBanner.vue
|
||
| }) | ||
|
|
||
| const body = computed(() => { | ||
| return hasTrackUnknownDevice.value | ||
| ? t('store-release-onboarding-body-track-unknown') | ||
| : hasTestFlightDevice.value | ||
| ? t('store-release-onboarding-body-testflight') | ||
| : t('store-release-onboarding-body') | ||
|
Check warning on line 83 in src/components/dashboard/StoreReleaseOnboardingBanner.vue
|
||
| }) | ||
|
|
||
| async function loadStatus() { | ||
| const requestId = ++loadStatusRequestId | ||
| const appId = props.appId | ||
|
|
||
| if (!appId) { | ||
| resetStatus() | ||
| isLoading.value = false | ||
| return | ||
| } | ||
|
|
||
| isLoading.value = true | ||
| try { | ||
| await organizationStore.awaitInitialLoad() | ||
|
|
||
| const [bundleResult, releaseDeviceCount, testDeviceCount, trackUnknownDeviceCount] = await Promise.all([ | ||
| supabase | ||
| .from('app_versions') | ||
| .select('id') | ||
| .eq('app_id', appId) | ||
| .eq('deleted', false) | ||
| .neq('name', 'unknown') | ||
| .neq('name', 'builtin') | ||
| .limit(1), | ||
| countDevicesByInstallSource(appId, RELEASE_INSTALL_SOURCES), | ||
| countDevicesByInstallSource(appId, TEST_INSTALL_SOURCES), | ||
| countDevicesByInstallSource(appId, TRACK_UNKNOWN_INSTALL_SOURCES), | ||
| ]) | ||
|
|
||
| if (requestId !== loadStatusRequestId) | ||
| return | ||
|
|
||
| if (bundleResult.error) { | ||
| console.error('Cannot load store release onboarding status', { | ||
| bundleError: bundleResult.error, | ||
| }) | ||
| resetStatus() | ||
| return | ||
| } | ||
|
|
||
| hasLiveUpdateBundle.value = (bundleResult.data?.length ?? 0) > 0 | ||
| hasStoreInstalledDevice.value = releaseDeviceCount > 0 | ||
| hasTestFlightDevice.value = testDeviceCount > 0 | ||
| hasTrackUnknownDevice.value = trackUnknownDeviceCount > 0 | ||
| } | ||
| catch (error) { | ||
| if (requestId !== loadStatusRequestId) | ||
| return | ||
| console.error('Cannot load store release onboarding status', error) | ||
| resetStatus() | ||
| } | ||
| finally { | ||
| if (requestId === loadStatusRequestId) | ||
| isLoading.value = false | ||
| } | ||
| } | ||
|
|
||
| function openBuilds() { | ||
| router.push(`/app/${encodeURIComponent(props.appId)}/builds`) | ||
| } | ||
|
|
||
| function openDevices() { | ||
| router.push(`/app/${encodeURIComponent(props.appId)}/devices`) | ||
| } | ||
|
|
||
| watch(() => [props.appId, organizationStore.currentOrganization?.gid], () => { | ||
| void loadStatus() | ||
| }, { immediate: true }) | ||
| </script> | ||
|
|
||
| <template> | ||
| <section | ||
| v-if="shouldShowBanner" | ||
| class="mb-4 overflow-hidden rounded-lg border border-amber-200 bg-white shadow-sm dark:border-amber-400/30 dark:bg-slate-900/95" | ||
| aria-live="polite" | ||
| > | ||
| <div class="flex flex-col gap-4 p-4 sm:p-5 lg:flex-row lg:items-center lg:justify-between"> | ||
| <div class="flex min-w-0 gap-4"> | ||
| <span class="flex h-11 w-11 shrink-0 items-center justify-center rounded-lg bg-amber-100 text-amber-700 ring-1 ring-amber-200 dark:bg-amber-400/15 dark:text-amber-200 dark:ring-amber-400/30"> | ||
| <IconStore class="h-5 w-5" /> | ||
| </span> | ||
| <div class="min-w-0"> | ||
| <p class="text-sm font-semibold uppercase text-amber-700 dark:text-amber-200"> | ||
| {{ t('store-release-onboarding-badge') }} | ||
| </p> | ||
| <h2 class="mt-1 text-xl font-semibold leading-snug text-slate-950 dark:text-white"> | ||
| {{ title }} | ||
| </h2> | ||
| <p class="mt-2 max-w-3xl text-sm leading-6 text-slate-600 dark:text-slate-300"> | ||
| {{ body }} | ||
| </p> | ||
|
|
||
| <ul class="mt-3 flex flex-col gap-2 text-sm text-slate-700 sm:flex-row sm:flex-wrap sm:items-center dark:text-slate-200"> | ||
| <li class="inline-flex items-center gap-2"> | ||
| <span class="flex h-6 w-6 items-center justify-center rounded-full bg-emerald-100 text-emerald-700 dark:bg-emerald-400/15 dark:text-emerald-200"> | ||
| <IconCheck class="h-3.5 w-3.5" /> | ||
| </span> | ||
| {{ t('store-release-onboarding-step-live-update') }} | ||
| </li> | ||
| <li v-if="hasTestFlightDevice" class="inline-flex items-center gap-2"> | ||
| <span class="flex h-6 w-6 items-center justify-center rounded-full bg-sky-100 text-sky-700 dark:bg-sky-400/15 dark:text-sky-200"> | ||
| <IconCheck class="h-3.5 w-3.5" /> | ||
| </span> | ||
| {{ t('store-release-onboarding-step-testflight') }} | ||
| </li> | ||
| <li v-if="hasTrackUnknownDevice" class="inline-flex items-center gap-2"> | ||
| <span class="flex h-6 w-6 items-center justify-center rounded-full bg-sky-100 text-sky-700 dark:bg-sky-400/15 dark:text-sky-200"> | ||
| <IconCircleDot class="h-3.5 w-3.5" /> | ||
| </span> | ||
| {{ t('store-release-onboarding-step-track-unknown') }} | ||
| </li> | ||
| <li class="inline-flex items-center gap-2"> | ||
| <span class="flex h-6 w-6 items-center justify-center rounded-full bg-amber-100 text-amber-700 dark:bg-amber-400/15 dark:text-amber-200"> | ||
| <IconCircleDot class="h-3.5 w-3.5" /> | ||
| </span> | ||
| {{ t('store-release-onboarding-step-store-release') }} | ||
| </li> | ||
| </ul> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div class="flex flex-col gap-2 sm:flex-row lg:shrink-0"> | ||
| <button class="d-btn d-btn-primary min-h-11" type="button" @click="openBuilds"> | ||
| <IconExternalLink class="h-4 w-4" /> | ||
| {{ t('store-release-onboarding-builds') }} | ||
| </button> | ||
| <button class="d-btn d-btn-outline min-h-11" type="button" @click="openDevices"> | ||
| <IconSmartphone class="h-4 w-4" /> | ||
| {{ t('store-release-onboarding-devices') }} | ||
| </button> | ||
| </div> | ||
| </div> | ||
| </section> | ||
| </template> | ||
Uh oh!
There was an error while loading. Please reload this page.