local RESOURCE_NAME = GetCurrentResourceName() local providers = SkyPhoneCompatibility.Providers local core = SkyPhoneApps.CompatibilityCore local debug_custom_app = SkyPhoneApps.Debug or function() end local provider_apps = {} local high_client_apps = {} local high_server_apps = {} local function get_calling_resource(export_name) local owner_resource = GetInvokingResource() if owner_resource then return owner_resource end Bridge.Debug("warn", "[%s] %s rejected: the export must be called by another resource.", RESOURCE_NAME, export_name ) return nil, "invalid_owner" end local function copy_record_data(value) if type(value) ~= "table" then return nil end local copied = {} for key, nested_value in pairs(value) do copied[key] = nested_value end return copied end local function register_provider_app(provider, owner_resource, definition, vendor_data) local app_id = definition.id local existing = provider_apps[app_id] debug_custom_app( "provider", "registration requested provider=%s id=%s owner=%s operation=%s", tostring(provider), tostring(app_id), tostring(owner_resource), existing and "update" or "add" ) if existing and (existing.owner_resource ~= owner_resource or existing.provider ~= provider) then debug_custom_app( "provider", "registration rejected provider=%s id=%s owner=%s existing_provider=%s existing_owner=%s error=duplicate_app_id", tostring(provider), tostring(app_id), tostring(owner_resource), tostring(existing.provider), tostring(existing.owner_resource) ) return false, "duplicate_app_id" end local success, error_message if existing then success, error_message = core.Update(owner_resource, definition) else success, error_message = core.Add(owner_resource, definition) end if success then provider_apps[app_id] = { definition = definition, owner_resource = owner_resource, provider = provider, vendor_data = vendor_data, } end debug_custom_app( "provider", "registration result provider=%s id=%s owner=%s success=%s error=%s", tostring(provider), tostring(app_id), tostring(owner_resource), tostring(success), tostring(error_message) ) return success, error_message end local function get_provider_app(owner_resource, app_id, allowed_providers) if type(app_id) ~= "string" then return nil, "invalid_app_id" end local record = provider_apps[app_id] if not record then return nil, "app_not_found" end if record.owner_resource ~= owner_resource then return nil, "app_owner_mismatch" end if allowed_providers and not allowed_providers[record.provider] then return nil, "app_provider_mismatch" end return record end local function remove_provider_app(owner_resource, app_id, allowed_providers) local record, record_error = get_provider_app(owner_resource, app_id, allowed_providers) if not record then return false, record_error end return core.Remove(owner_resource, app_id) end function SkyPhoneApps.RegisterCompatibilityExport(owner_resource, app_data) debug_custom_app( "provider", "compatibility export received owner=%s identifier=%s key=%s data_type=%s", tostring(owner_resource), type(app_data) == "table" and tostring(app_data.identifier) or "nil", type(app_data) == "table" and tostring(app_data.key) or "nil", type(app_data) ) local provider local definition local definition_error if app_data.identifier ~= nil then provider = providers.lb definition, definition_error = SkyPhoneCompatibility.BuildLbDefinition(owner_resource, app_data) elseif app_data.key ~= nil then provider = providers.yseries definition, definition_error = SkyPhoneCompatibility.BuildYSeriesDefinition(app_data) else debug_custom_app( "provider", "compatibility export rejected owner=%s error=unknown_app_provider", tostring(owner_resource) ) return false, "unknown_app_provider" end if not definition then debug_custom_app( "provider", "definition rejected provider=%s owner=%s error=%s", tostring(provider), tostring(owner_resource), tostring(definition_error) ) Bridge.Debug("warn", "[%s] %s registration rejected for %s: %s.", RESOURCE_NAME, provider, owner_resource, definition_error ) return false, definition_error end debug_custom_app( "provider", "definition built provider=%s id=%s owner=%s ui=%s icon=%s default_installed=%s fix_blur=%s", tostring(provider), tostring(definition.id), tostring(owner_resource), tostring(definition.ui), tostring(definition.icon), tostring(definition.defaultInstalled), definition.compatibility and tostring(definition.compatibility.fixBlur) or "nil" ) return register_provider_app(provider, owner_resource, definition, copy_record_data(app_data)) end SkyPhoneApps.OnCompatibilityAppRemoved = function(owner_resource, app_id) local record = provider_apps[app_id] if record and record.owner_resource == owner_resource then provider_apps[app_id] = nil end end local function add_17mov_application(app_data) local owner_resource, owner_error = get_calling_resource("AddApplication") if not owner_resource then return false, owner_error end local definition, definition_error = SkyPhoneCompatibility.Build17MovDefinition(app_data) if not definition then Bridge.Debug("warn", "[%s] 17mov registration rejected for %s: %s.", RESOURCE_NAME, owner_resource, definition_error ) return false, definition_error end return register_provider_app( providers.seventeen, owner_resource, definition, copy_record_data(app_data) ) end local function remove_17mov_application(app_name, _resource_name) local owner_resource, owner_error = get_calling_resource("RemoveApplication") if not owner_resource then return false, owner_error end local app_id = type(app_name) == "table" and app_name.name or app_name return remove_provider_app(owner_resource, app_id, { [providers.seventeen] = true, }) end local function send_app_message(app_id, data) local owner_resource, owner_error = get_calling_resource("SendAppMessage") if not owner_resource then return false, owner_error end local record, record_error = get_provider_app(owner_resource, app_id, { [providers.seventeen] = true, [providers.yseries] = true, }) if not record then return false, record_error end return core.SendMessage(owner_resource, app_id, data) end local function add_high_application(app_name, data, locales) local owner_resource, owner_error = get_calling_resource("addApplication") if not owner_resource then return false, owner_error end local definition, definition_error = SkyPhoneCompatibility.BuildHighDefinition( owner_resource, app_name, data, locales ) if not definition then Bridge.Debug("warn", "[%s] High Phone registration rejected for %s: %s.", RESOURCE_NAME, owner_resource, definition_error ) return false, definition_error end high_client_apps[app_name] = { definition = definition, owner_resource = owner_resource, } local server_record = high_server_apps[app_name] if server_record then if server_record.owner_resource ~= owner_resource then return false, "duplicate_app_id" end if server_record.registered then return true end return false, "server_definition_not_registered" end return register_provider_app( providers.high, owner_resource, definition, copy_record_data(data) ) end local function send_high_app_nui(app_name, data) local owner_resource, owner_error = get_calling_resource("sendAppNui") if not owner_resource then return false, owner_error end local record, record_error = get_provider_app(owner_resource, app_name, { [providers.high] = true, }) if not record then return false, record_error end return core.SendMessage(owner_resource, app_name, data) end local function add_quasar_app_for_owner(owner_resource, app_data) local definition, definition_error = SkyPhoneCompatibility.BuildQuasarDefinition(app_data) if not definition then Bridge.Debug("warn", "[%s] Quasar registration rejected for %s: %s.", RESOURCE_NAME, owner_resource, definition_error ) return false, definition_error end return register_provider_app( providers.quasar, owner_resource, definition, SkyPhoneCompatibility.CopyQuasarData(app_data) ) end local function add_quasar_app(app_data) local owner_resource, owner_error = get_calling_resource("addCustomApp") if not owner_resource then return false, owner_error end if type(app_data) ~= "table" then return false, "invalid_app_data" end return add_quasar_app_for_owner(owner_resource, app_data) end local function add_quasar_apps_batch(apps) local owner_resource, owner_error = get_calling_resource("addCustomAppsBatch") if not owner_resource then return false, owner_error end if type(apps) ~= "table" then return false, "invalid_app_batch" end for index = 1, #apps do local success, error_message = add_quasar_app_for_owner(owner_resource, apps[index]) if not success then return false, error_message end end return true end local function update_quasar_app(app_id, patch) local owner_resource, owner_error = get_calling_resource("updateCustomApp") if not owner_resource then return false, owner_error end if type(patch) ~= "table" then return false, "invalid_patch" end local record, record_error = get_provider_app(owner_resource, app_id, { [providers.quasar] = true, }) if not record then return false, record_error end local merged = SkyPhoneCompatibility.CopyQuasarData(record.vendor_data) for key, value in pairs(patch) do if key == "iframe" and type(value) == "table" then merged.iframe = merged.iframe or {} for iframe_key, iframe_value in pairs(value) do merged.iframe[iframe_key] = iframe_value end elseif key ~= "id" then merged[key] = value end end merged.id = app_id local definition, definition_error = SkyPhoneCompatibility.BuildQuasarDefinition(merged) if not definition then return false, definition_error end local success, error_message = core.Update(owner_resource, definition) if success then record.definition = definition record.vendor_data = SkyPhoneCompatibility.CopyQuasarData(merged) end return success, error_message end local function remove_quasar_app(app_id) local owner_resource, owner_error = get_calling_resource("removeCustomApp") if not owner_resource then return false, owner_error end return remove_provider_app(owner_resource, app_id, { [providers.quasar] = true, }) end local function get_quasar_apps() local ids = {} for app_id, record in pairs(provider_apps) do if record.provider == providers.quasar then ids[#ids + 1] = app_id end end table.sort(ids) local apps = {} for index = 1, #ids do apps[index] = SkyPhoneCompatibility.CopyQuasarData(provider_apps[ids[index]].vendor_data) end return apps end local function open_quasar_app(app_id) local owner_resource, owner_error = get_calling_resource("OpenPhoneApp") if not owner_resource then return false, owner_error end local record, record_error = get_provider_app(owner_resource, app_id, { [providers.quasar] = true, }) if not record then return false, record_error end return core.Open(owner_resource, app_id) end local function open_phone_app(app_id, data) local owner_resource, owner_error = get_calling_resource("OpenApp") if not owner_resource then return false, owner_error end local record, record_error = get_provider_app(owner_resource, app_id) if not record then return false, record_error end return core.Open(owner_resource, app_id, data) end local function close_phone_app(options) local owner_resource, owner_error = get_calling_resource("CloseApp") if not owner_resource then return false, owner_error end local app_id = type(options) == "table" and options.app or nil if app_id then local record, record_error = get_provider_app(owner_resource, app_id, { [providers.lb] = true, [providers.yseries] = true, }) if not record then return false, record_error end return core.Close(owner_resource, app_id) end return core.CloseActive(owner_resource) end local function normalize_lb_notification_text(value, maximum_length, error_code) if type(value) ~= "string" then return nil, error_code end local normalized = value:match("^%s*(.-)%s*$") if normalized == "" or #normalized > maximum_length then return nil, error_code end return normalized end local function send_lb_notification(data) local owner_resource, owner_error = get_calling_resource("SendNotification") if not owner_resource then return false, owner_error end if type(data) ~= "table" then return false, "invalid_notification" end local app_id = data.app or data.identifier local valid_app_id, app_id_error = SkyPhoneApps.ValidateAppId(app_id) if not valid_app_id then return false, app_id_error end local registered_record = provider_apps[app_id] if registered_record then local record, record_error = get_provider_app(owner_resource, app_id, { [providers.lb] = true, }) if not record then return false, record_error end elseif not SkyPhoneApps.ReservedAppIds[app_id] then return false, "app_not_found" end local title, title_error = normalize_lb_notification_text( data.title, 128, "invalid_notification_title" ) if not title then return false, title_error end local content, content_error = normalize_lb_notification_text( data.content or data.message or data.text, 512, "invalid_notification_text" ) if not content then return false, content_error end SendNUIMessage({ type = "notification:show", data = { appId = app_id, route = "/apps/" .. app_id, text = content, title = title, }, }) return true end local function register_high_server_app(owner_resource, definition, revision) if type(owner_resource) ~= "string" or type(definition) ~= "table" or type(definition.id) ~= "string" or type(revision) ~= "number" or revision ~= math.floor(revision) or revision < 1 then Bridge.Debug("warn", "[%s] Rejected invalid High Phone server application snapshot.", RESOURCE_NAME) return nil end local app_id = definition.id local existing = high_server_apps[app_id] if existing and revision <= existing.revision then if revision < existing.revision or existing.registered then return app_id end if existing.owner_resource ~= owner_resource then Bridge.Debug("warn", "[%s] Rejected conflicting High Phone owner for %s.", RESOURCE_NAME, app_id ) return app_id end end local provider_record = provider_apps[app_id] local success, error_message local retained_registration = false if provider_record then if provider_record.owner_resource ~= owner_resource or provider_record.provider ~= providers.high then success, error_message = false, "duplicate_app_id" else retained_registration = true success, error_message = core.Update(owner_resource, definition) end else success, error_message = core.Add(owner_resource, definition) end high_server_apps[app_id] = { definition = definition, last_error = success and nil or error_message, owner_resource = owner_resource, registered = success or retained_registration, revision = revision, } if success then provider_apps[app_id] = { definition = definition, owner_resource = owner_resource, provider = providers.high, } elseif not existing or existing.revision ~= revision or existing.last_error ~= error_message then Bridge.Debug("error", "[%s] Could not register High Phone server application %s revision %s: %s.", RESOURCE_NAME, app_id, revision, error_message or "unknown_error" ) end return app_id end local function remove_high_server_app(owner_resource, app_id) local record = high_server_apps[app_id] if not record or record.owner_resource ~= owner_resource then return end high_server_apps[app_id] = nil if record.registered then core.Remove(owner_resource, app_id) end local client_record = high_client_apps[app_id] if client_record then local success, error_message = register_provider_app( providers.high, client_record.owner_resource, client_record.definition, nil ) if not success then Bridge.Debug("error", "[%s] Could not restore High Phone client application %s: %s.", RESOURCE_NAME, app_id, error_message or "unknown_error" ) end end end RegisterNetEvent("sky_phone:compat:high:client:syncApplication", function(owner_resource, definition, revision) if source ~= 65535 then Bridge.Debug("warn", "[%s] Rejected locally invoked High Phone application sync.", RESOURCE_NAME) return end register_high_server_app(owner_resource, definition, revision) end) RegisterNetEvent("sky_phone:compat:high:client:removeApplication", function(owner_resource, app_id) if source ~= 65535 then Bridge.Debug("warn", "[%s] Rejected locally invoked High Phone application removal.", RESOURCE_NAME) return end if type(owner_resource) ~= "string" or type(app_id) ~= "string" then Bridge.Debug("warn", "[%s] Rejected invalid High Phone application removal.", RESOURCE_NAME) return end remove_high_server_app(owner_resource, app_id) end) RegisterNetEvent("sky_phone:compat:high:client:replaceSnapshot", function(snapshot) if source ~= 65535 then Bridge.Debug("warn", "[%s] Rejected locally invoked High Phone application snapshot.", RESOURCE_NAME) return end if type(snapshot) ~= "table" then Bridge.Debug("warn", "[%s] Rejected invalid High Phone application snapshot.", RESOURCE_NAME) return end local seen = {} for index = 1, #snapshot do local record = snapshot[index] if type(record) == "table" then local app_id = register_high_server_app( record.owner_resource, record.definition, record.revision ) if app_id then seen[app_id] = true end end end local removed = {} for app_id, record in pairs(high_server_apps) do if not seen[app_id] then removed[#removed + 1] = { app_id = app_id, owner_resource = record.owner_resource, } end end for index = 1, #removed do remove_high_server_app(removed[index].owner_resource, removed[index].app_id) end end) AddEventHandler("onClientResourceStart", function(resource_name) for _, record in pairs(high_server_apps) do if record.owner_resource == resource_name and not record.registered then register_high_server_app(record.owner_resource, record.definition, record.revision) end end end) AddEventHandler("onClientResourceStop", function(resource_name) for app_id, record in pairs(high_client_apps) do if record.owner_resource == resource_name then high_client_apps[app_id] = nil end end end) exports("AddApplication", add_17mov_application) exports("RemoveApplication", remove_17mov_application) exports("SendAppMessage", send_app_message) exports("addApplication", add_high_application) exports("sendAppNui", send_high_app_nui) exports("addCustomApp", add_quasar_app) exports("addCustomAppsBatch", add_quasar_apps_batch) exports("updateCustomApp", update_quasar_app) exports("removeCustomApp", remove_quasar_app) exports("getCustomApps", get_quasar_apps) exports("OpenPhoneApp", open_quasar_app) exports("OpenApp", open_phone_app) exports("CloseApp", close_phone_app) exports("SendNotification", send_lb_notification) SkyPhoneCompatibility.RegisterExportAlias("lb-phone", "OpenApp", open_phone_app) SkyPhoneCompatibility.RegisterExportAlias("lb-phone", "CloseApp", close_phone_app) SkyPhoneCompatibility.RegisterExportAlias("lb-phone", "SendNotification", send_lb_notification) SkyPhoneCompatibility.RegisterExportAlias("17mov_Phone", "AddApplication", add_17mov_application) SkyPhoneCompatibility.RegisterExportAlias("17mov_Phone", "RemoveApplication", remove_17mov_application) SkyPhoneCompatibility.RegisterExportAlias("17mov_Phone", "SendAppMessage", send_app_message) SkyPhoneCompatibility.RegisterExportAlias("high-phone", "addApplication", add_high_application) SkyPhoneCompatibility.RegisterExportAlias("high-phone", "sendAppNui", send_high_app_nui) SkyPhoneCompatibility.RegisterExportAlias("qs-smartphone", "addCustomApp", add_quasar_app) SkyPhoneCompatibility.RegisterExportAlias("qs-smartphone", "addCustomAppsBatch", add_quasar_apps_batch) SkyPhoneCompatibility.RegisterExportAlias("qs-smartphone", "updateCustomApp", update_quasar_app) SkyPhoneCompatibility.RegisterExportAlias("qs-smartphone", "removeCustomApp", remove_quasar_app) SkyPhoneCompatibility.RegisterExportAlias("qs-smartphone", "getCustomApps", get_quasar_apps) SkyPhoneCompatibility.RegisterExportAlias("qs-smartphone", "OpenPhoneApp", open_quasar_app) SkyPhoneCompatibility.RegisterExportAlias("yseries", "SendAppMessage", send_app_message) SkyPhoneCompatibility.RegisterExportAlias("yseries", "CloseApp", close_phone_app) SkyPhoneCompatibility.RegisterExportAlias("yseries", "GetDataLoaded", function() return true end) CreateThread(function() debug_custom_app("provider", "requesting provider snapshots and emitting compatibility ready signals") TriggerServerEvent("sky_phone:compat:high:server:requestSnapshot") TriggerEvent("17mov_Phone:Client:Ready") local provider_resources = { "lb-phone", "17mov_Phone", "high-phone", "qs-smartphone", "yseries", } for index = 1, #provider_resources do debug_custom_app( "provider", "emitting onResourceStart compatibility signal for %s (state=%s)", provider_resources[index], tostring(GetResourceState(provider_resources[index])) ) TriggerEvent("onResourceStart", provider_resources[index]) end end)