local is_open = false local open_requested = false local notification_focus = false local device_payload = nil local sim_picker_open = false local call_channel = 0 Bridge.Debug("debug", "[sky_phone] Client script initialized.", { always = true }) local server_callbacks = { "security:unlock", "security:set-passcode", "security:change-passcode", "security:disable-passcode", "device:save", "notifications:save", "device:factory-reset", "account:login", "account:register", "account:logout", "account:devices", "account:remove-device", "notes:list", "notes:create", "notes:update", "notes:delete", "mail:register", "mail:login", "mail:logout", "mail:counts", "mail:list", "mail:get", "mail:get-draft", "mail:save-draft", "mail:delete-draft", "mail:send", "mail:set-read", "mail:trash", "mail:restore", "mail:delete-forever", "mail:empty-trash", "marketplace:list", "marketplace:get", "marketplace:list-own", "marketplace:create", "marketplace:update", "marketplace:set-status", "marketplace:favorite", "marketplace:counts", "marketplace:list-inquiries", "marketplace:get-inquiry", "marketplace:send-message", "marketplace:make-offer", "marketplace:respond-offer", "marketplace:report", "marketplace:block", "pages:list", "pages:get", "pages:list-own", "pages:create", "pages:share-citymarkt", "pages:react", "pages:delete", "fliptok:register", "fliptok:login", "fliptok:logout", "fliptok:bootstrap", "fliptok:feed", "fliptok:discover", "fliptok:publish", "fliptok:react", "fliptok:follow", "fliptok:comments", "fliptok:comment", "fliptok:view", "fliptok:share", "fliptok:profile", "fliptok:update-profile", "fliptok:activities", "fliptok:mark-activities", "fliptok:report", "fliptok:admin-reports", "fliptok:admin-resolve-report", "fliptok:block", "fliptok:delete", "picstagram:register", "picstagram:login", "picstagram:logout", "picstagram:bootstrap", "picstagram:feed", "picstagram:explore", "picstagram:search", "picstagram:saved", "picstagram:profile", "picstagram:update-profile", "picstagram:publish-post", "picstagram:update-post", "picstagram:set-post-status", "picstagram:react", "picstagram:follow", "picstagram:respond-follow", "picstagram:comments", "picstagram:comment", "picstagram:remove-comment", "picstagram:publish-story", "picstagram:stories", "picstagram:view-story", "picstagram:story-viewers", "picstagram:remove-story", "picstagram:activities", "picstagram:mark-activities", "picstagram:block", "picstagram:report", "picstagram:admin-reports", "picstagram:admin-resolve-report", "feather:bootstrap", "feather:create-profile", "feather:update-profile", "feather:feed", "feather:explore", "feather:network", "feather:create-post", "feather:react", "feather:follow", "feather:connections", "feather:remove-connection", "feather:profile", "feather:bookmarks", "feather:thread", "feather:activities", "feather:mark-activities", "feather:delete", "feather:block", "feather:report", "calendar:list", "calendar:create", "calendar:update", "calendar:delete", "music:bootstrap", "music:add-youtube", "music:remove-youtube", "music:create-playlist", "music:rename-playlist", "music:delete-playlist", "music:add-to-playlist", "music:remove-from-playlist", "map:markers", "map:create-marker", "map:delete-marker", "crewlink:bootstrap", "crewlink:create-profile", "crewlink:update-profile", "crewlink:create-group", "crewlink:update-group", "crewlink:delete-group", "crewlink:set-active", "crewlink:join-code", "crewlink:rotate-code", "crewlink:nearby", "crewlink:invite-nearby", "crewlink:respond-invite", "crewlink:update-member", "crewlink:transfer-owner", "crewlink:remove-member", "crewlink:leave", "crewlink:create-ping", "crewlink:remove-ping", "crewlink:live", "sim:insert", "sim:eject", "contacts:list", "contacts:save", "contacts:delete", "contacts:favorite", "calls:recents", "calls:dial", "calls:answer", "calls:decline", "calls:hangup", "calls:block", "banking:overview", "banking:transfer", "billing:overview", "billing:list", "billing:detail", "billing:markRead", "billing:pay", "billing:dispute", "messages:conversations", "messages:thread", "messages:send", "messages:media", "messages:delete", "messages:gifs", "easyshare:bootstrap", "easyshare:set-visibility", "easyshare:request", "easyshare:respond", "easyshare:cancel", "darkchat:bootstrap", "darkchat:update-profile", "darkchat:start", "darkchat:thread", "darkchat:send", "darkchat:media", "darkchat:react", "darkchat:message-action", "darkchat:update-conversation", "darkchat:add-contact", "darkchat:remove-contact", "darkchat:block", "darkchat:report", "darkchat:clear", "flare:bootstrap", "flare:save-profile", "flare:set-discovery", "flare:swipe", "flare:rewind", "flare:unmatch", "flare:thread", "flare:send", "gallery:list", "media:config", } local function get_locale() return Locales[Config.Bridge.Locale] or Locales["en"] end local function send_open_message() if not device_payload then return end local payload = device_payload payload.lang = Config.Bridge.Locale payload.locales = get_locale().Nui SendNUIMessage({ type = "app:open", data = payload, }) end local function open_phone() if is_open or not device_payload then return end send_open_message() end local function close_phone() open_requested = false TriggerEvent("sky_phone:animation:phone", false) if not is_open then return end is_open = false TriggerEvent("sky_phone:nuiClosed") SetNuiFocus(notification_focus or sim_picker_open, notification_focus or sim_picker_open) SendNUIMessage({ type = "app:close" }) Bridge.Callbacks.Trigger("sky_phone:device:close", {}) end local function leave_call_voice() if call_channel == 0 then return end if Config.Calls.VoiceProvider == "pma" and GetResourceState("pma-voice") == "started" then exports["pma-voice"]:setCallChannel(0) end call_channel = 0 end local function join_call_voice(channel) if Config.Calls.VoiceProvider ~= "pma" then Bridge.Debug("error", "[sky_phone] Unsupported voice provider '%s'.", tostring(Config.Calls.VoiceProvider)) return false end if GetResourceState("pma-voice") ~= "started" then Bridge.Debug("error", "[sky_phone] Configured pma-voice provider is not started.") return false end call_channel = tonumber(channel) or 0 exports["pma-voice"]:setCallChannel(call_channel) return true end if Config.Phone.DevelopmentCommand then RegisterCommand(Config.Command, function() if is_open then close_phone() return end Bridge.Callbacks.Trigger("sky_phone:device:development-open", {}) end, false) end RegisterNUICallback("ui:ready", function(_, cb) Bridge.Debug("debug", "[sky_phone] NUI reported ready.", { always = true }) TriggerEvent("sky_phone:client:nuiReady") if open_requested and device_payload then send_open_message() end cb({ success = true }) end) RegisterNUICallback("ui:opened", function(_, cb) 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 } ) cb({ success = false, error = "open_not_requested" }) return end is_open = true notification_focus = false SetNuiFocus(true, true) TriggerEvent("sky_phone:animation:phone", true) cb({ success = true }) end) RegisterNUICallback("close", function(_, cb) close_phone() cb({ success = true }) end) RegisterNUICallback("notification:focus", function(data, cb) notification_focus = data.active == true and not is_open SetNuiFocus(is_open or notification_focus, is_open or notification_focus) cb({ success = true }) end) RegisterNUICallback("sim:picker-close", function(_, cb) sim_picker_open = false SetNuiFocus(is_open or notification_focus, is_open or notification_focus) cb({ success = true }) end) RegisterNUICallback("map:getPlayerCoords", function(_, cb) local coords = GetEntityCoords(PlayerPedId()) cb({ success = true, data = { coords = { x = coords.x, y = coords.y, z = coords.z } } }) end) RegisterNUICallback("map:setWaypoint", function(data, cb) local coords = type(data) == "table" and data.coords or nil local x = type(coords) == "table" and tonumber(coords.x) or nil local y = type(coords) == "table" and tonumber(coords.y) or nil if not x or not y or x ~= x or y ~= y or math.abs(x) > 10000.0 or math.abs(y) > 10000.0 then cb({ success = false, error = "invalid_marker" }) return end SetNewWaypoint(x, y) cb({ success = true }) end) local weather_types = { [joaat("EXTRASUNNY")] = "sunny", [joaat("CLEAR")] = "clear", [joaat("CLOUDS")] = "partly_cloudy", [joaat("OVERCAST")] = "cloudy", [joaat("RAIN")] = "rain", [joaat("CLEARING")] = "rain", [joaat("THUNDER")] = "thunder", [joaat("SMOG")] = "fog", [joaat("FOGGY")] = "fog", [joaat("NEUTRAL")] = "cloudy", [joaat("SNOW")] = "snow", [joaat("BLIZZARD")] = "snow", [joaat("SNOWLIGHT")] = "snow", [joaat("XMAS")] = "snow", [joaat("HALLOWEEN")] = "cloudy", } local function weather_region(coords) if coords.x > 2500.0 and coords.y < -3000.0 then return "cayo_perico" end if coords.y > 900.0 then return "blaine_county" end return "los_santos" end RegisterNUICallback("weather:get", function(_, cb) local coords = GetEntityCoords(PlayerPedId()) local weather_hash = GetPrevWeatherTypeHashName() local next_weather_hash = GetNextWeatherTypeHashName() cb({ success = true, data = { condition = weather_types[weather_hash] or "clear", nextCondition = weather_types[next_weather_hash] or weather_types[weather_hash] or "clear", region = weather_region(coords), clock = { year = GetClockYear(), month = GetClockMonth() + 1, day = GetClockDayOfMonth(), hour = GetClockHours(), minute = GetClockMinutes(), }, windSpeed = math.max(0.0, GetWindSpeed()), rainLevel = math.max(0.0, math.min(1.0, GetRainLevel())), }, }) end) local function garage_vehicle_kind(model_hash, fallback) if IsThisModelABoat(model_hash) then return "boat" end if IsThisModelAPlane(model_hash) then return "plane" end if IsThisModelAHeli(model_hash) then return "helicopter" end if IsThisModelABike(model_hash) or IsThisModelABicycle(model_hash) then return "bike" end return fallback end RegisterNUICallback("garage:vehicles", function(data, cb) local result = Bridge.Callbacks.Trigger("sky_phone:garage:vehicles", data) if not result or not result.success or type(result.data) ~= "table" then cb(result or { success = false, error = "request_failed" }) return end for _, vehicle in ipairs(result.data.vehicles or {}) do local model_hash = tonumber(vehicle.model) if not model_hash and type(vehicle.model) == "string" and vehicle.model ~= "" then model_hash = joaat(vehicle.model) end if model_hash then local display_name = GetDisplayNameFromVehicleModel(model_hash) local label = display_name and GetLabelText(display_name) or nil if label and label ~= "NULL" and label ~= "CARNOTFOUND" then vehicle.name = label elseif type(vehicle.model) == "string" and vehicle.model ~= "" then vehicle.name = vehicle.model end vehicle.kind = garage_vehicle_kind(model_hash, vehicle.kind) end end cb(result) end) for _, callback_name in ipairs(server_callbacks) do RegisterNUICallback(callback_name, function(data, cb) local result = Bridge.Callbacks.Trigger("sky_phone:" .. callback_name, data) if result then cb(result) return end cb({ success = false, error = "request_failed" }) end) end RegisterNetEvent("sky_phone:device:open", function(data) 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 open_requested = true open_phone() end) RegisterNetEvent("sky_phone:device:updated", function(data) device_payload = data SendNUIMessage({ type = "device:updated", data = data }) end) RegisterNetEvent("sky_phone:device:invalidated", function() open_requested = false device_payload = nil TriggerEvent("sky_phone:animation:reset") close_phone() end) RegisterNetEvent("sky_phone:device:error", function(error_code) Bridge.Debug( "debug", "[sky_phone] Client received device error: %s.", tostring(error_code), { always = true } ) local message = get_locale().DeviceErrors[error_code] or get_locale().DeviceErrors.default Bridge.Framework.Notify("iFruit", message, "error", 5000) end) RegisterNetEvent("sky_phone:mail:changed", function(data) SendNUIMessage({ type = "mail:changed", data = data }) end) RegisterNetEvent("sky_phone:easyshare:changed", function(data) SendNUIMessage({ type = "easyshare:changed", data = data }) end) RegisterNetEvent("sky_phone:gallery:changed", function() SendNUIMessage({ type = "gallery:changed" }) end) RegisterNetEvent("sky_phone:mail:new", function(data) local mail_locale = get_locale().Nui.Apps.mail data.title = mail_locale.name data.text = mail_locale.newMessage:gsub("{sender}", tostring(data.sender)) SendNUIMessage({ type = "mail:new", data = data }) end) RegisterNetEvent("sky_phone:marketplace:changed", function(data) SendNUIMessage({ type = "marketplace:changed", data = data }) end) RegisterNetEvent("sky_phone:fliptok:command-feedback", function(data) Bridge.Framework.Notify("FlipTok", data.message, data.notificationType, 5000) end) RegisterNetEvent("sky_phone:fliptok:verification-changed", function(data) SendNUIMessage({ type = "fliptok:verification-changed", data = data }) end) RegisterNetEvent("sky_phone:fliptok:new", function(data) local fliptok_locale = get_locale().Nui.Apps.fliptok local notification_text = fliptok_locale.notifications[data.kind] or fliptok_locale.notifications.default data.title = fliptok_locale.name data.text = notification_text:gsub("{actor}", tostring(data.actor or "")) SendNUIMessage({ type = "fliptok:new", data = data }) end) RegisterNetEvent("sky_phone:picstagram:command-feedback", function(data) Bridge.Framework.Notify("Picstagram", data.message, data.notificationType, 5000) end) RegisterNetEvent("sky_phone:picstagram:verification-changed", function(data) SendNUIMessage({ type = "picstagram:verification-changed", data = data }) end) RegisterNetEvent("sky_phone:picstagram:new", function(data) local picstagram_locale = get_locale().Nui.Apps.picstagram local notification_text = picstagram_locale.notifications[data.kind] or picstagram_locale.notifications.default data.title = picstagram_locale.name data.text = notification_text:gsub("{actor}", tostring(data.actor or "")) SendNUIMessage({ type = "picstagram:new", data = data }) end) RegisterNetEvent("sky_phone:feather:new", function(data) local feather_locale = get_locale().Nui.Apps.feather local notification_text = feather_locale.notifications[data.kind] or feather_locale.notifications.default data.title = feather_locale.name data.text = notification_text:gsub("{actor}", tostring(data.actor or "")) SendNUIMessage({ type = "feather:new", data = data }) end) RegisterNetEvent("sky_phone:marketplace:new-message", function(data) local marketplace_locale = get_locale().Nui.Apps.citymarkt data.title = marketplace_locale.name if data.kind == "offer" then data.text = marketplace_locale.newOffer :gsub("{sender}", tostring(data.sender)) :gsub("{price}", tostring(data.amount)) elseif data.kind == "offer-response" and data.action == "accepted" then data.text = marketplace_locale.offerAcceptedNotification :gsub("{sender}", tostring(data.sender)) :gsub("{price}", tostring(data.amount)) elseif data.kind == "offer-response" and data.action == "rejected" then data.text = marketplace_locale.offerRejectedNotification :gsub("{sender}", tostring(data.sender)) :gsub("{price}", tostring(data.amount)) else data.text = marketplace_locale.newMessage:gsub("{sender}", tostring(data.sender)) end SendNUIMessage({ type = "marketplace:new-message", data = data }) end) RegisterNetEvent("sky_phone:calendar:reminder", function(data) local calendar_locale = get_locale().Nui.Apps.calendar data.title = calendar_locale.name data.text = calendar_locale.reminder:gsub("{title}", tostring(data.eventTitle)) SendNUIMessage({ type = "calendar:reminder", data = data }) end) RegisterNetEvent("sky_phone:flare:match", function(data) local flare_locale = get_locale().Nui.Apps.flare data.title = flare_locale.name data.text = flare_locale.newMatchNotification:gsub("{sender}", tostring(data.sender)) SendNUIMessage({ type = "flare:new-match", data = data }) end) RegisterNetEvent("sky_phone:flare:message", function(data) local flare_locale = get_locale().Nui.Apps.flare data.title = flare_locale.name data.text = flare_locale.newMessageNotification:gsub("{sender}", tostring(data.sender)) SendNUIMessage({ type = "flare:new-message", data = data }) end) RegisterNetEvent("sky_phone:sim:picker", function(data) sim_picker_open = true SetNuiFocus(true, true) SendNUIMessage({ type = "sim:picker", data = data }) end) RegisterNetEvent("sky_phone:sim:picker-close", function() sim_picker_open = false SetNuiFocus(is_open or notification_focus, is_open or notification_focus) SendNUIMessage({ type = "sim:picker-close" }) end) RegisterNetEvent("sky_phone:contacts:changed", function() SendNUIMessage({ type = "contacts:changed" }) end) RegisterNetEvent("sky_phone:calls:changed", function() SendNUIMessage({ type = "calls:changed" }) end) RegisterNetEvent("sky_phone:banking:changed", function() SendNUIMessage({ type = "banking:changed" }) end) RegisterNetEvent("sky_phone:billing:changed", function() SendNUIMessage({ type = "billing:changed" }) end) RegisterNetEvent("sky_phone:billing:new", function(data) local billing_locale = get_locale().Nui.Apps.billing data.title = billing_locale.name data.text = billing_locale.notifications.newInvoice :gsub("{issuer}", tostring(data.issuer)) :gsub("{amount}", ("%s %s"):format(tostring(data.amount), Config.Billing.Currency)) SendNUIMessage({ type = "billing:new", data = data }) end) RegisterNetEvent("sky_phone:crewlink:changed", function(data) SendNUIMessage({ type = "crewlink:changed", data = data }) end) RegisterNetEvent("sky_phone:crewlink:notification", function(data) local crewlink_locale = get_locale().Nui.Apps.crewlink local notification_text = crewlink_locale.notifications[data.kind] or crewlink_locale.notifications.default data.title = crewlink_locale.name data.text = notification_text :gsub("{actor}", tostring(data.actor or "")) :gsub("{group}", tostring(data.groupName or "")) :gsub("{ping}", tostring(data.pingLabel or "")) SendNUIMessage({ type = "crewlink:notification", data = data }) end) RegisterNetEvent("sky_phone:messages:changed", function(data) SendNUIMessage({ type = "messages:changed", data = data }) end) RegisterNetEvent("sky_phone:messages:new", function(data) local messages_locale = get_locale().Nui.Apps.messages data.title = messages_locale.name data.text = messages_locale.newMessage:gsub("{sender}", tostring(data.sender)) SendNUIMessage({ type = "messages:new", data = data }) end) RegisterNetEvent("sky_phone:darkchat:changed", function(data) SendNUIMessage({ type = "darkchat:changed", data = data }) end) RegisterNetEvent("sky_phone:darkchat:new", function(data) local darkchat_locale = get_locale().Nui.Apps.darkchat data.title = darkchat_locale.name if data.notificationMode == "private" then data.sender = nil data.text = darkchat_locale.privateNotification elseif data.notificationMode == "hidden" then data.sender = nil data.text = "" else data.text = darkchat_locale.newMessage:gsub("{sender}", tostring(data.sender)) end SendNUIMessage({ type = "darkchat:new", data = data }) end) RegisterNetEvent("sky_phone:call:incoming", function(data) notification_focus = true SetNuiFocus(true, true) TriggerEvent("sky_phone:animation:call", data) SendNUIMessage({ type = "call:incoming", data = data }) end) RegisterNetEvent("sky_phone:call:state", function(data) if data.state == "connected" and data.channel then if not join_call_voice(data.channel) then TriggerEvent("sky_phone:device:error", "voice_unavailable") end elseif data.state ~= "ringing" then leave_call_voice() end TriggerEvent("sky_phone:animation:call", data) SendNUIMessage({ type = "call:state", data = data }) end) CreateThread(function() if Config.Phone.DevelopmentCommand then TriggerEvent("chat:addSuggestion", "/" .. Config.Command, get_locale().CommandDescription) end end) AddEventHandler("onResourceStop", function(resource_name) if resource_name ~= GetCurrentResourceName() then return end if is_open or notification_focus then SetNuiFocus(false, false) end TriggerEvent("sky_phone:animation:reset") leave_call_voice() if Config.Phone.DevelopmentCommand then TriggerEvent("chat:removeSuggestion", "/" .. Config.Command) end end)