SkyPhoneClient = {} local is_open = false local open_requested = false local open_without_focus = false local device_payload = nil local equipped_phone_number = nil local nui_generation = 0 local live_activity_active = false local open_home_requested = false local function get_equipped_phone_number() if not device_payload or not device_payload.device.sim then return nil end return device_payload.device.sim.number end local function set_equipped_phone_number(next_number) if next_number ~= nil and type(next_number) ~= "string" then next_number = nil end if next_number == equipped_phone_number then return end equipped_phone_number = next_number TriggerEvent("sky_phone:client:phoneNumberChanged", next_number) end local function update_equipped_phone_number(data) set_equipped_phone_number(type(data) == "table" and type(data.device) == "table" and type(data.device.sim) == "table" and data.device.sim.number or nil) end local function get_authoritative_phone_number() local result = Bridge.Callbacks.Trigger("sky_phone:device:equipped-number", {}) if type(result) ~= "table" or not result.success or type(result.data) ~= "table" then return nil end local phone_number = result.data.phoneNumber set_equipped_phone_number(phone_number) return phone_number end local function get_phone_state() return { inCall = SkyPhoneCalls.IsActive(), onScreen = is_open, open = is_open, phoneNumber = get_equipped_phone_number(), } end SkyPhoneClient.GetState = get_phone_state SkyPhoneClient.GetEquippedPhoneNumber = get_authoritative_phone_number Bridge.Debug("debug", "[sky_phone] Client script initialized.", { always = true }) local locale, locale_name = SkyPhoneLocales.Resolve(Config.Bridge.Locale) local function send_open_message() if not device_payload then return end local payload = device_payload payload.lang = locale_name payload.locales = locale.Nui payload.fallbackLocales = Locales.en.Nui SkyPhoneApps.SendCatalog() SendNUIMessage({ type = "app:open", data = payload, openHome = open_home_requested, }) open_home_requested = false end local function open_phone() if is_open or not device_payload then return end send_open_message() end local function close_phone(close_device_session) local was_requested = open_requested local was_open = is_open open_requested = false open_without_focus = false TriggerEvent("sky_phone:animation:phone", false) is_open = false if was_open then SkyPhoneApps.SetPhoneOpen(false) TriggerEvent("sky_phone:nuiClosed") TriggerEvent("sky_phone:client:phoneToggled", false) end SkyPhoneFocus.SetPhone(false) if was_requested or was_open then SendNUIMessage({ type = "app:close" }) if close_device_session ~= false then Bridge.Callbacks.Trigger("sky_phone:device:close", {}) end end end local function toggle_phone(open, no_focus) if open ~= nil and type(open) ~= "boolean" then Bridge.Debug("error", "[sky_phone] Rejected invalid phone toggle state.") return false end if no_focus ~= nil and type(no_focus) ~= "boolean" then Bridge.Debug("error", "[sky_phone] Rejected invalid phone toggle focus state.") return false end local should_open = open if should_open == nil then should_open = not (is_open or open_requested) end if not should_open then close_phone() return true end open_without_focus = no_focus == true if is_open or open_requested then if is_open then SkyPhoneFocus.SetPhone(true, open_without_focus) end return true end local result = Bridge.Callbacks.Trigger("sky_phone:device:open-request", {}) if type(result) ~= "table" or result.success ~= true then open_without_focus = false return false end return true end SkyPhoneClient.Toggle = toggle_phone AddEventHandler("sky_phone:client:forceClose", function() close_phone() end) if Config.Phone.DevelopmentCommand then RegisterCommand(Config.Command, function() if is_open or open_requested then close_phone() return end open_without_focus = false Bridge.Callbacks.Trigger("sky_phone:device:development-open", {}) end, false) end RegisterCommand("sky_phone_toggle", function() if is_open or open_requested then close_phone() return end open_without_focus = false Bridge.Callbacks.Trigger("sky_phone:device:open-request", {}) end, false) RegisterCommand("sky_phone_live_activity_open", function() if not live_activity_active or is_open or open_requested then return end open_home_requested = true local result = Bridge.Callbacks.Trigger("sky_phone:device:open-request", {}) if not result or not result.success then open_home_requested = false end end, false) RegisterKeyMapping( "sky_phone_live_activity_open", locale.Controls.OpenPhone, "keyboard", "SPACE" ) if Config.Phone.Keybind then if type(Config.Phone.Keybind) ~= "string" or Config.Phone.Keybind == "" then error("[sky_phone] Config.Phone.Keybind must be a non-empty keyboard key name or false.") end RegisterKeyMapping("sky_phone_toggle", locale.Controls.OpenPhone, "keyboard", Config.Phone.Keybind) end RegisterNetEvent("sky_phone:testdata:feedback", function(success, detail) local test_data_locale = locale.TestData local message = success and test_data_locale.Success or test_data_locale.Failed if success and type(detail) == "string" and detail ~= "" then message = message:gsub("{email}", detail) end Bridge.Framework.Notify("iFruit", message, success and "success" or "error", 7000) end) RegisterNUICallback("ui:ready", function(data, cb) if type(data) ~= "table" or data.protocolVersion ~= 1 then cb({ success = false, error = "unsupported_protocol" }) return end nui_generation = nui_generation + 1 -- Browser state is recreated on a CEF reload. Browser-owned focus claims -- cannot survive unless their UI is replayed as part of this handshake. SkyPhoneFocus.BeginNuiHydration() Bridge.Debug("debug", "[sky_phone] NUI reported ready.", { always = true }) SkyPhoneApps.SendCatalog() if open_requested and device_payload then send_open_message() end SkyPhoneCalls.ReplayNui() SkyPhoneSimPicker.ReplayNui() SkyPhoneFocus.Reapply() TriggerEvent("sky_phone:client:nuiReady", { generation = nui_generation, protocolVersion = 1, }) cb({ success = true, data = { generation = nui_generation, protocolVersion = 1, }, }) end) RegisterNUICallback("ui:opened", function(data, cb) if type(data) ~= "table" then cb({ success = false, error = "invalid_request" }) return end if not open_requested or not device_payload then Bridge.Debug( "warn", "[sky_phone] Ignored a NUI open confirmation without a pending device open.", { always = true } ) SendNUIMessage({ type = "app:close" }) SkyPhoneFocus.Reapply() cb({ success = false, error = "open_not_requested" }) return end local was_open = is_open is_open = true SkyPhoneApps.SetPhoneOpen(true) if not was_open then TriggerEvent("sky_phone:client:phoneToggled", true) end SkyPhoneFocus.SetPhone(true, open_without_focus) TriggerEvent("sky_phone:animation:phone", true) cb({ success = true }) end) RegisterNUICallback("ui:input-focus", function(data, cb) if type(data) ~= "table" or type(data.active) ~= "boolean" then cb({ success = false, error = "invalid_request" }) return end SkyPhoneFocus.SetTextInputFocused(data.active and (is_open or open_requested)) cb({ success = true }) end) RegisterNUICallback("ui:live-activity", function(data, cb) if type(data) ~= "table" or type(data.active) ~= "boolean" then cb({ success = false, error = "invalid_request" }) return end live_activity_active = data.active cb({ success = true }) end) RegisterNUICallback("close", function(data, cb) if type(data) ~= "table" then cb({ success = false, error = "invalid_request" }) return end close_phone() cb({ success = true }) end) RegisterNetEvent("sky_phone:device:open", function(data) if type(data) ~= "table" or type(data.device) ~= "table" or type(data.device.imei) ~= "string" then Bridge.Debug("error", "[sky_phone] Rejected invalid device open data.") return end Bridge.Debug( "debug", "[sky_phone] Client received device open for IMEI %s account_linked=%s.", tostring(data.device.imei), tostring(data.account ~= nil), { always = true } ) device_payload = data update_equipped_phone_number(data) open_requested = true if is_open then SkyPhoneApps.SendCatalog() SendNUIMessage({ type = "device:updated", data = data }) return end open_phone() end) RegisterNetEvent("sky_phone:device:updated", function(data) if type(data) ~= "table" or type(data.device) ~= "table" or type(data.device.imei) ~= "string" then Bridge.Debug("error", "[sky_phone] Rejected invalid device update data.") return end device_payload = data update_equipped_phone_number(data) SendNUIMessage({ type = "device:updated", data = data }) end) RegisterNetEvent("sky_phone:device:invalidated", function() TriggerEvent("sky_phone:animation:reset") close_phone(false) device_payload = nil update_equipped_phone_number(nil) live_activity_active = false open_home_requested = false end) RegisterNetEvent("sky_phone:device:error", function(error_code) if not is_open and not open_requested then open_without_focus = false end Bridge.Debug( "debug", "[sky_phone] Client received device error: %s.", tostring(error_code), { always = true } ) local message = locale.DeviceErrors[error_code] or locale.DeviceErrors.default Bridge.Framework.Notify("iFruit", message, "error", 5000) end) CreateThread(function() if Config.Phone.DevelopmentCommand then TriggerEvent("chat:addSuggestion", "/" .. Config.Command, locale.CommandDescription) end if Config.TestData.Enabled then TriggerEvent("chat:addSuggestion", "/" .. Config.TestData.Command, locale.TestData.CommandDescription) end end) AddEventHandler("onResourceStop", function(resource_name) if resource_name ~= GetCurrentResourceName() then return end is_open = false open_requested = false open_without_focus = false TriggerEvent("sky_phone:animation:reset") SkyPhoneCalls.Reset() SkyPhoneSimPicker.Reset() SkyPhoneFocus.Reset() if Config.Phone.DevelopmentCommand then TriggerEvent("chat:removeSuggestion", "/" .. Config.Command) end if Config.TestData.Enabled then TriggerEvent("chat:removeSuggestion", "/" .. Config.TestData.Command) end end)