mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-28 17:01:18 +00:00
FIX - await setup persistence before completion (#9)
This commit is contained in:
@@ -17,7 +17,9 @@ describe('PhoneSetupAssistant contract', () => {
|
||||
expect(source).toContain('WALLPAPER_IDS')
|
||||
expect(source).toContain('setAllAppNotifications')
|
||||
expect(source).toContain('appStore.claimApp')
|
||||
expect(source).toContain('phone.completeSetup()')
|
||||
expect(source).toContain('await phone.completeSetup()')
|
||||
expect(source).toContain(':disabled="setupCompleteBusy"')
|
||||
expect(source).toContain("phone.t('Setup.ready.saveFailed')")
|
||||
})
|
||||
|
||||
it('persists progress and supports resuming or moving backward', () => {
|
||||
|
||||
@@ -53,6 +53,8 @@ const passcodeLength = ref<4 | 6>(phone.security.length === 4 ? 4 : 6)
|
||||
const notificationsEnabled = ref(true)
|
||||
const notificationSounds = ref(true)
|
||||
const selectedApps = ref<BuiltinPhoneAppId[]>(['banking', 'garage', 'skyride'])
|
||||
const setupCompleteBusy = ref(false)
|
||||
const setupCompleteError = ref('')
|
||||
|
||||
const setupApps = (
|
||||
['banking', 'garage', 'skyride', 'citymarkt', 'picstagram', 'snake'] as const
|
||||
@@ -204,8 +206,16 @@ function choosePasscodeLength(length: 4 | 6): void {
|
||||
passcodeResetKey.value += 1
|
||||
}
|
||||
|
||||
function finish(): void {
|
||||
phone.completeSetup()
|
||||
async function finish(): Promise<void> {
|
||||
if (setupCompleteBusy.value) return
|
||||
setupCompleteBusy.value = true
|
||||
setupCompleteError.value = ''
|
||||
const completed = await phone.completeSetup()
|
||||
setupCompleteBusy.value = false
|
||||
if (!completed) {
|
||||
setupCompleteError.value = phone.t('Setup.ready.saveFailed')
|
||||
return
|
||||
}
|
||||
emit('complete')
|
||||
}
|
||||
|
||||
@@ -794,12 +804,29 @@ function skipSetupForDevelopment(): void {
|
||||
}}</b></span
|
||||
>
|
||||
</div>
|
||||
<SkyButton class="setup-assistant__primary" @click="finish">{{
|
||||
phone.t('Setup.ready.enter')
|
||||
}}</SkyButton>
|
||||
<SkyButton
|
||||
class="setup-assistant__primary"
|
||||
:disabled="setupCompleteBusy"
|
||||
@click="finish"
|
||||
>
|
||||
<SkySpinner
|
||||
v-if="setupCompleteBusy"
|
||||
:label="phone.t('Setup.ready.saving')"
|
||||
:size="18"
|
||||
/>
|
||||
<span v-else>{{ phone.t('Setup.ready.enter') }}</span>
|
||||
</SkyButton>
|
||||
<p
|
||||
v-if="setupCompleteError"
|
||||
class="setup-assistant__error"
|
||||
role="alert"
|
||||
>
|
||||
{{ setupCompleteError }}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
class="setup-assistant__later"
|
||||
:disabled="setupCompleteBusy"
|
||||
@click="moveTo(0)"
|
||||
>
|
||||
{{ phone.t('Setup.ready.review') }}
|
||||
|
||||
@@ -85,4 +85,16 @@ describe('phone inventory contracts', () => {
|
||||
)
|
||||
expect(phoneServer).toContain('preferred_device_imeis[source] = imei')
|
||||
})
|
||||
|
||||
it('keeps non-unique phones bound to one persistent device per character', () => {
|
||||
const phoneServer = readResourceFile('source/server/phone.lua')
|
||||
const migration = readResourceFile('source/server/db_migrate.lua')
|
||||
|
||||
expect(phoneServer).toContain('if not unique_phones then')
|
||||
expect(phoneServer).toContain('return map_character_device(source, slot)')
|
||||
expect(phoneServer).toContain('FROM `sky_phone_character_devices`')
|
||||
expect(phoneServer).toContain('WHERE `owner_identifier` = ?')
|
||||
expect(migration).toContain('name = "sky_phone_character_devices"')
|
||||
expect(migration).toContain('primaryKey = "owner_identifier"')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -167,4 +167,66 @@ describe('phone device persistence scope', () => {
|
||||
expect(mockNuiCall).toHaveBeenCalledTimes(2)
|
||||
expect(phone.deviceRevisions.widgets).toBe(1)
|
||||
})
|
||||
|
||||
it('marks setup complete only after the settings save is acknowledged', async () => {
|
||||
const completion = deferredResponse<{ revision: number }>()
|
||||
mockNuiCall.mockReturnValueOnce(completion.promise)
|
||||
const phone = usePhoneStore()
|
||||
phone.open({
|
||||
device: {
|
||||
data: {
|
||||
settings: {
|
||||
payload: {
|
||||
settings: { setupCompleted: false, setupStep: 9 },
|
||||
version: 1,
|
||||
},
|
||||
revision: 3,
|
||||
},
|
||||
},
|
||||
imei: '111',
|
||||
name: 'Phone 111',
|
||||
sim: null,
|
||||
},
|
||||
token: 'session-a',
|
||||
})
|
||||
|
||||
const completed = phone.completeSetup()
|
||||
await Promise.resolve()
|
||||
|
||||
expect(phone.preferences.settings.setupCompleted).toBe(false)
|
||||
completion.resolve({ data: { revision: 4 }, success: true })
|
||||
|
||||
await expect(completed).resolves.toBe(true)
|
||||
expect(phone.preferences.settings.setupCompleted).toBe(true)
|
||||
expect(phone.deviceRevisions.settings).toBe(4)
|
||||
})
|
||||
|
||||
it('keeps setup open when the completion save is rejected', async () => {
|
||||
mockNuiCall.mockResolvedValueOnce({
|
||||
error: 'request_failed',
|
||||
success: false,
|
||||
})
|
||||
const phone = usePhoneStore()
|
||||
phone.open({
|
||||
device: {
|
||||
data: {
|
||||
settings: {
|
||||
payload: {
|
||||
settings: { setupCompleted: false, setupStep: 9 },
|
||||
version: 1,
|
||||
},
|
||||
revision: 3,
|
||||
},
|
||||
},
|
||||
imei: '111',
|
||||
name: 'Phone 111',
|
||||
sim: null,
|
||||
},
|
||||
token: 'session-a',
|
||||
})
|
||||
|
||||
await expect(phone.completeSetup()).resolves.toBe(false)
|
||||
expect(phone.preferences.settings.setupCompleted).toBe(false)
|
||||
expect(phone.deviceRevisions.settings).toBe(3)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -43,7 +43,7 @@ export type PhoneOpenPayload = {
|
||||
token?: string
|
||||
}
|
||||
|
||||
const namespaceQueues = new Map<string, Promise<void>>()
|
||||
const namespaceQueues = new Map<string, Promise<boolean>>()
|
||||
let nextPersistenceSession = 0
|
||||
|
||||
const companiesFallbackLocales = {
|
||||
@@ -4949,6 +4949,8 @@ const defaultLocales: LocaleTree = {
|
||||
localOnly: 'Stored on this phone',
|
||||
enter: 'Enter Sky Phone',
|
||||
review: 'Review Setup',
|
||||
saveFailed: 'Setup could not be saved. Try again.',
|
||||
saving: 'Saving setup',
|
||||
},
|
||||
},
|
||||
Common: {
|
||||
@@ -5223,13 +5225,13 @@ export const usePhoneStore = defineStore('phone', {
|
||||
JSON.stringify(device.data.settings?.payload ?? null),
|
||||
)
|
||||
},
|
||||
saveDeviceNamespace(namespace: string, payload: unknown): void {
|
||||
saveDeviceNamespace(namespace: string, payload: unknown): Promise<boolean> {
|
||||
const imei = this.device?.imei
|
||||
if (!imei) {
|
||||
console.error(
|
||||
`[Phone persistence] Could not save ${namespace} without an active device.`,
|
||||
)
|
||||
return
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
const generation = this.persistenceGeneration
|
||||
const session = this.persistenceSession
|
||||
@@ -5243,7 +5245,7 @@ export const usePhoneStore = defineStore('phone', {
|
||||
this.deviceSessionToken === token
|
||||
const previous = namespaceQueues.get(queueKey) ?? Promise.resolve()
|
||||
const queued = previous.then(async () => {
|
||||
if (!isCurrentScope()) return
|
||||
if (!isCurrentScope()) return false
|
||||
const response = await nuiCall<{ revision: number }>('device:save', {
|
||||
imei,
|
||||
namespace,
|
||||
@@ -5258,13 +5260,21 @@ export const usePhoneStore = defineStore('phone', {
|
||||
Number(response.data?.revision) >= 0
|
||||
) {
|
||||
this.deviceRevisions[namespace] = Number(response.data?.revision)
|
||||
return true
|
||||
}
|
||||
if (isCurrentScope()) {
|
||||
console.error(
|
||||
`[Phone persistence] Could not save ${namespace}: ${response.error ?? 'request_failed'}`,
|
||||
)
|
||||
}
|
||||
return false
|
||||
})
|
||||
const tracked = queued.finally(() => {
|
||||
if (namespaceQueues.get(queueKey) === tracked)
|
||||
namespaceQueues.delete(queueKey)
|
||||
})
|
||||
namespaceQueues.set(queueKey, tracked)
|
||||
return tracked
|
||||
},
|
||||
async flushDevicePersistence(): Promise<void> {
|
||||
const imei = this.device?.imei
|
||||
@@ -5329,10 +5339,16 @@ export const usePhoneStore = defineStore('phone', {
|
||||
)
|
||||
this.saveDeviceNamespace('settings', this.preferences)
|
||||
},
|
||||
completeSetup(): void {
|
||||
this.preferences.settings.setupCompleted = true
|
||||
this.preferences.settings.setupStep = PHONE_SETUP_LAST_STEP
|
||||
this.saveDeviceNamespace('settings', this.preferences)
|
||||
async completeSetup(): Promise<boolean> {
|
||||
const completedPreferences = cloneJsonData(this.preferences)
|
||||
completedPreferences.settings.setupCompleted = true
|
||||
completedPreferences.settings.setupStep = PHONE_SETUP_LAST_STEP
|
||||
const saved = await this.saveDeviceNamespace(
|
||||
'settings',
|
||||
completedPreferences,
|
||||
)
|
||||
if (saved) this.preferences = completedPreferences
|
||||
return saved
|
||||
},
|
||||
resetAfterFactoryReset(): void {
|
||||
this.persistenceGeneration += 1
|
||||
|
||||
@@ -123,6 +123,7 @@ Locales["de"] = {
|
||||
ready = {
|
||||
eyebrow = "Einrichtung abgeschlossen", title = "Willkommen, {name}", body = "Dein Sky Phone ist eingerichtet und bereit. Du kannst alles später in den Einstellungen anpassen.",
|
||||
localOnly = "Auf diesem Handy gespeichert", enter = "Sky Phone öffnen", review = "Einrichtung prüfen",
|
||||
saving = "Einrichtung wird gespeichert", saveFailed = "Die Einrichtung konnte nicht gespeichert werden. Versuche es erneut.",
|
||||
},
|
||||
},
|
||||
Common = {
|
||||
|
||||
@@ -123,6 +123,7 @@ Locales["en"] = {
|
||||
ready = {
|
||||
eyebrow = "Setup Complete", title = "Welcome, {name}", body = "Your Sky Phone is configured and ready. Your choices can always be refined in Settings.",
|
||||
localOnly = "Stored on this phone", enter = "Enter Sky Phone", review = "Review Setup",
|
||||
saving = "Saving setup", saveFailed = "Setup could not be saved. Try again.",
|
||||
},
|
||||
},
|
||||
Common = {
|
||||
|
||||
Reference in New Issue
Block a user