-
-
-
None
-
The developer has not specified any permissions required by this app.
+
diff --git a/css/app.css b/css/app.css
index 2761ec9..464c83c 100644
--- a/css/app.css
+++ b/css/app.css
@@ -172,6 +172,63 @@ a {
opacity: 0.5;
}
+#permission-containers {
+ display: flex;
+ flex-direction: column;
+ gap: 1.75em;
+ margin-top: 1em;
+ margin-bottom: 0.5em;
+}
+
+.permission-icon {
+ color: var(--app-tint-color) !important;
+ font-size: 2em;
+}
+
+.permission-container {
+ border-radius: 1.5em;
+ padding: 1.25em;
+ box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.15);
+}
+
+.permission-container-header {
+ text-align: center;
+ margin-bottom: 0.5em;
+}
+
+.permission-container p.description {
+ opacity: 0.5;
+ line-height: 1.5em;
+}
+
+.permission-container p {
+ font-size: 0.9em;
+}
+
+.permission-container b {
+ font-size: 1.25em;
+}
+
+.permission-items {
+ margin: 0.5em 0;
+ display: grid;
+ grid-template-columns: 49.25% 49.25%;
+ row-gap: 4px;
+}
+
+.permission-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ width: fit-content;
+ font-size: 0.85em;
+ color: black;
+}
+
+.permission-item i {
+ font-size: 1.5em !important;
+}
+
/* Source */
#source {
padding-top: 0;
diff --git a/css/style.css b/css/style.css
index 9b3b882..b5e886f 100644
--- a/css/style.css
+++ b/css/style.css
@@ -12,12 +12,14 @@
--accent-color: #018084;
--app-tint-color: #018084;
- --color-bg: rgba(255, 255, 255, 255);
+ --color-bg: rgba(255, 255, 255, 1);
+ --color-bg-secondary: rgba(235, 235, 235, 1);
--color-separator: rgba(0, 0, 0, 0.15);
- --color-primary: rgba(0, 122, 254, 255);
+ --color-primary: rgba(0, 122, 254, 1);
--color-transparent: rgba(255, 255, 255, 0.75);
--color-bg-dark: rgb(26, 25, 27);
+ --color-bg-dark-secondary: rgb(46, 45, 47);
--color-separator-dark: rgba(255, 255, 255, 0.15);
--color-primary-dark: rgba(11, 132, 254, 255);
--color-transparent-dark: rgba(26, 25, 27, 0.25);
@@ -64,6 +66,14 @@
.screenshot {
border-color: rgba(255, 255, 255, 0.2) !important;
}
+
+ .secondary-bg {
+ background-color: var(--color-bg-dark-secondary) !important;
+ }
+}
+
+.secondary-bg {
+ background-color: white;
}
body.loading {
diff --git a/js/app.js b/js/app.js
index ae7be43..d6b9ad7 100644
--- a/js/app.js
+++ b/js/app.js
@@ -6,9 +6,11 @@
// MIT License.
//
-import { urlSearchParams, sourceURL } from "./constants.js";
-import { formatString } from "./utilities.js";
+import { urlSearchParams, sourceURL, legacyPermissions } from "./constants.js";
+import { formatString, insertSpaceInCamelString, insertSpaceInSnakeString } from "./utilities.js";
import { main } from "./main.js";
+import { privacy, entitlements } from "./constants.js";
+import { AppPermissionItem } from "./components/AppPermissionItem.js";
if (!urlSearchParams.has('id')) exit();
const bundleId = urlSearchParams.get('id');
@@ -160,95 +162,57 @@ main((json) => {
//
// Permissions
- const permissions = document.getElementById("permissions");
- // If permissions specified
- if (app.permissions) {
- // Remove placeholder permission
- permissions.querySelector(".permission").remove();
+ //
+ // Privacy
+ const privacyContainer = document.getElementById("privacy");
+ if (app.appPermissions?.privacy?.length || app.permissions) {
+ privacyContainer.querySelector(".permission-icon").classList = "permission-icon bi-person-fill-lock";
+ privacyContainer.querySelector("b").innerText = "Privacy";
+ privacyContainer.querySelector(".description").innerText = `"${app.name}" may request to access the following:`;
+ }
+ app.appPermissions?.privacy?.forEach(privacyPermission => {
+ const permission = privacy[privacyPermission.name];
+ let name = permission?.name ?? insertSpaceInCamelString(privacyPermission.name),
+ icon;
+ if (permission?.icon) icon = permission.icon;
+ else icon = "gear-wide-connected";
+ privacyContainer.querySelector(".permission-items").insertAdjacentHTML("beforeend",
+ AppPermissionItem(name, icon, privacyPermission?.usageDescription)
+ );
+ });
- app.permissions?.forEach(permission => {
- var permissionType, icon;
- switch (permission.type) {
- // AltStore-supported permissions
- case "background-audio":
- permissionType = "Background Audio";
- icon = "volume-up-fill";
- break;
- case "background-fetch":
- permissionType = "Background Fetch";
- icon = "arrow-repeat"
- break;
- case "photos":
- permissionType = "Photos"
- icon = "image-fill";
- break;
- // Additional permissions
- case "camera":
- permissionType = "Camera"
- icon = "camera-fill";
- break;
- case "music":
- permissionType = "Music Library"
- icon = "music-note-list";
- break;
- case "location":
- permissionType = "Location"
- icon = "geo-alt-fill";
- break;
- case "microphone":
- permissionType = "Microphone"
- icon = "mic-fill";
- break;
- case "contacts":
- permissionType = "Contacts"
- icon = "people-fill";
- break;
- case "bluetooth":
- permissionType = "Bluetooth"
- icon = "bluetooth";
- break;
- case "faceid":
- permissionType = "Face ID"
- icon = "person-bounding-box";
- break;
- case "network":
- permissionType = "Network"
- icon = "wifi";
- break;
- case "calendar":
- case "calendars":
- permissionType = "Calendar"
- icon = "calendar-date";
- break;
- case "reminders":
- permissionType = "Reminders"
- icon = "list-ul";
- break;
- case "siri":
- permissionType = "Siri"
- icon = "gear-wide-connected";
- break;
- case "speech-recognition":
- permissionType = "Speech Recognition"
- icon = "soundwave";
- break;
- default:
- permissionType = permission.type.replaceAll("-", " ");
- icon = "gear-wide-connected";
- break;
- }
- permissions.insertAdjacentHTML("beforeend", `
-
-
-
-
${permissionType}
-
${permission.usageDescription ?? "No description provided."}
-
-
`);
+ //
+ // Legacy permissions
+ if (!app.appPermissions?.privacy) {
+ app.permissions?.forEach(appPermission => {
+ const permission = legacyPermissions[appPermission.type];
+ let name = insertSpaceInSnakeString(appPermission.type),
+ icon;
+ if (permission?.icon) icon = permission.icon;
+ else icon = "gear-wide-connected";
+ privacyContainer.querySelector(".permission-items").insertAdjacentHTML("beforeend",
+ AppPermissionItem(name, icon, appPermission?.usageDescription)
+ );
});
}
+ //
+ // Entitlements
+ const entitlementsContainer = document.getElementById("entitlements");
+ console.log(app.appPermissions?.entitlements);
+ if (!app.appPermissions?.entitlements?.length) entitlementsContainer.remove();
+ app.appPermissions?.entitlements.forEach(entitlementPermission => {
+ const permission = entitlements[entitlementPermission.name];
+ let name = permission?.name ?? insertSpaceInSnakeString(entitlementPermission.name),
+ icon;
+ if (permission?.icon) icon = permission.icon;
+ else icon = "gear-wide-connected";;
+ entitlementsContainer.querySelector(".permission-items").insertAdjacentHTML("beforeend",
+ AppPermissionItem(name, icon, permission?.description)
+ );
+ });
+
//
// Source info
const source = document.getElementById("source");
diff --git a/js/components/AppPermissionItem.js b/js/components/AppPermissionItem.js
new file mode 100644
index 0000000..ce2ceba
--- /dev/null
+++ b/js/components/AppPermissionItem.js
@@ -0,0 +1,15 @@
+// AppPermissionItem.js
+// altsource-viewer (https://github.com/therealFoxster/altsource-viewer)
+//
+// Copyright (c) 2023 Foxster.
+// MIT License.
+//
+
+export const AppPermissionItem = (name, icon, details) => `
+
+
+ ${name}
+
+`;
\ No newline at end of file
diff --git a/js/constants.js b/js/constants.js
index 7ebebcf..5f1b7f8 100644
--- a/js/constants.js
+++ b/js/constants.js
@@ -7,6 +7,123 @@
//
export const urlSearchParams = new URLSearchParams(window.location.search);
+
export const sourceURL = urlSearchParams.get('source')?.replaceAll("+", "%2B");
+
// https://stackoverflow.com/a/8943487
-export const urlRegex = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
\ No newline at end of file
+export const urlRegex = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
+
+export const entitlements = {
+ "com.apple.security.application-groups": {
+ name: "App Groups",
+ description: "Allow app to share files with other apps and app extensions in the same App Group.",
+ icon: "columns-gap"
+ },
+ "com.apple.developer.associated-domains": {
+ name: "Associated Domains",
+ description: "The associated domains for specific services, such as shared web credentials, universal links, and App Clips.",
+ icon: "globe2"
+ },
+ "com.apple.developer.carplay-audio": {
+ name: "CarPlay Audio",
+ description: "Allows the app the provide audio content for CarPlay.",
+ icon: "car-front-fill"
+ },
+ "get-task-allow": {
+ name: "Debuggable",
+ description: "Allow developers to attach a debugger to this app. This permission is required for JIT to work.",
+ icon: "tools"
+ },
+ "com.apple.developer.device-information.user-assigned-device-name": {
+ name: "Device Name",
+ description: "Grants access to the user-assigned device name instead of a generic device name.",
+ icon: "phone-fill"
+ },
+ "keychain-access-groups": {
+ name: "Keychain",
+ description: "Allows app to read and write secure data to the system's keychain.",
+ icon: "key-fill"
+ },
+ "com.apple.developer.networking.multicast": {
+ name: "Multicast",
+ description: "App can send or receive IP multicast traffic.",
+ icon: "globe2"
+ },
+ "aps-environment": {
+ name: "Push Notifications",
+ description: "App can send push notifications.",
+ icon: "app-indicator"
+ },
+ "com.apple.developer.applesignin": {
+ name: "Sign in with Apple",
+ description: "Allows sign in with Apple.",
+ icon: "apple"
+ },
+ "com.apple.developer.siri": {
+ name: "Siri",
+ description: "Allows app to handle Siri requests.",
+ icon: "mic-fill"
+ },
+ "com.apple.developer.networking.wifi-info": {
+ name: "Wi-Fi Information Access",
+ description: "Allows app to access information about the connected Wi-Fi network.",
+ icon: "wifi"
+ }
+};
+
+export const privacy = {
+ "AppleMusic": {
+ icon: "music-note-beamed"
+ },
+ "BluetoothAlways": {
+ name: "Bluetooth",
+ icon: "bluetooth"
+ },
+ "BluetoothPeripheral": {
+ name: "Bluetooth (Peripherals)",
+ icon: "bluetooth"
+ },
+ "Contacts": {
+ icon: "person-circle"
+ },
+ "Camera": {
+ icon: "camera-fill"
+ },
+ "FaceID": {
+ name: "Face ID",
+ icon: "person-bounding-box"
+ },
+ "LocalNetwork": {
+ icon: "globe2"
+ },
+ "LocationWhenInUse": {
+ name: "Location (When Using)",
+ icon: "cursor-fill"
+ },
+ "Microphone": {
+ icon: "mic-fill"
+ },
+ "PhotoLibrary": {
+ name: "Photos",
+ icon: "images"
+ },
+ "PhotoLibraryAdd": {
+ name: "Photos (Add)",
+ icon: "image"
+ },
+ "UserTracking": {
+ icon: "person-vcard-fill"
+ },
+}
+
+export const legacyPermissions = {
+ "background-audio": {
+ icon: "volume-up-fill"
+ },
+ "background-fetch": {
+ icon: "arrow-repeat"
+ },
+ "photos": {
+ icon: "images"
+ }
+}
\ No newline at end of file
diff --git a/js/utilities.js b/js/utilities.js
index 586b592..c4c0458 100644
--- a/js/utilities.js
+++ b/js/utilities.js
@@ -10,6 +10,15 @@ import { AltStoreBanner } from "./components/AltStoreBanner.js";
import { NavigationBar } from "./components/NavigationBar.js";
import { urlRegex } from "./constants.js";
+export function insertSpaceInSnakeString(string) {
+ return string.split(".").slice(-1)[0].split("-").map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
+}
+
+export function insertSpaceInCamelString(string) {
+ // https://stackoverflow.com/a/38388188/19227228
+ return string.match(/[A-Z][a-z]+|[0-9]+/g).join(" ");
+}
+
export function insertAltStoreBanner(sourceName) {
document.getElementById("top")?.insertAdjacentHTML("afterbegin", AltStoreBanner(sourceName));
}