expo-notifications
Advanced tools
@@ -71,3 +71,3 @@ import { CodedError, Platform, SyntheticPlatformEmitter } from '@unimodules/core'; | ||
// https://stackoverflow.com/a/35729334/2603230 | ||
const notificationIcon = (Constants.manifest.notification || {}).icon; | ||
const notificationIcon = (Constants.manifest?.notification || {}).icon; | ||
await registration.active.postMessage(JSON.stringify({ fromExpoWebClient: { notificationIcon } })); | ||
@@ -74,0 +74,0 @@ return subscriptionObject; |
@@ -11,3 +11,3 @@ import { Platform, CodedError, UnavailabilityError } from '@unimodules/core'; | ||
const deviceId = options.deviceId || (await getDeviceIdAsync()); | ||
const experienceId = options.experienceId || Constants.manifest?.currentFullName || Constants.manifest?.id; | ||
const experienceId = options.experienceId || Constants.manifest?.originalFullName || Constants.manifest?.id; | ||
if (!experienceId) { | ||
@@ -14,0 +14,0 @@ throw new CodedError('ERR_NOTIFICATIONS_NO_EXPERIENCE_ID', "No experienceId found. If it can't be inferred from the manifest (eg. in bare workflow), you have to pass it in yourself."); |
@@ -27,2 +27,4 @@ export { default as getDevicePushTokenAsync } from './getDevicePushTokenAsync'; | ||
export { setAutoServerRegistrationEnabledAsync } from './DevicePushTokenAutoRegistration.fx'; | ||
export { default as registerTaskAsync } from './registerTaskAsync'; | ||
export { default as unregisterTaskAsync } from './unregisterTaskAsync'; | ||
export * from './TokenEmitter'; | ||
@@ -29,0 +31,0 @@ export * from './NotificationsEmitter'; |
@@ -27,2 +27,4 @@ export { default as getDevicePushTokenAsync } from './getDevicePushTokenAsync'; | ||
export { setAutoServerRegistrationEnabledAsync } from './DevicePushTokenAutoRegistration.fx'; | ||
export { default as registerTaskAsync } from './registerTaskAsync'; | ||
export { default as unregisterTaskAsync } from './unregisterTaskAsync'; | ||
export * from './TokenEmitter'; | ||
@@ -29,0 +31,0 @@ export * from './NotificationsEmitter'; |
@@ -1,2 +0,2 @@ | ||
import { PermissionResponse } from 'unimodules-permissions-interface'; | ||
import { PermissionResponse } from 'expo-modules-core'; | ||
export declare enum IosAlertStyle { | ||
@@ -3,0 +3,0 @@ NONE = 0, |
import { Platform } from '@unimodules/core'; | ||
import { PermissionStatus } from 'unimodules-permissions-interface'; | ||
import { PermissionStatus } from 'expo-modules-core'; | ||
function convertPermissionStatus(status) { | ||
@@ -4,0 +4,0 @@ switch (status) { |
import { UnavailabilityError } from '@unimodules/core'; | ||
import uuidv4 from 'uuid/v4'; | ||
import { v4 as uuidv4 } from 'uuid'; | ||
import NotificationPresenter from './NotificationPresenterModule'; | ||
@@ -4,0 +4,0 @@ let warningMessageShown = false; |
import { Platform, UnavailabilityError } from '@unimodules/core'; | ||
import uuidv4 from 'uuid/v4'; | ||
import { v4 as uuidv4 } from 'uuid'; | ||
import NotificationScheduler from './NotificationScheduler'; | ||
@@ -4,0 +4,0 @@ export default async function scheduleNotificationAsync(request) { |
import { CodedError } from '@unimodules/core'; | ||
import uuidv4 from 'uuid/v4'; | ||
import { v4 as uuidv4 } from 'uuid'; | ||
const INSTALLATION_ID_KEY = 'EXPO_NOTIFICATIONS_INSTALLATION_ID'; | ||
@@ -4,0 +4,0 @@ const REGISTRATION_INFO_KEY = 'EXPO_NOTIFICATIONS_REGISTRATION_INFO'; |
@@ -11,8 +11,23 @@ # Changelog | ||
## 0.11.6 — 2021-04-21 | ||
### 💡 Others | ||
## 0.12.0 — 2021-06-16 | ||
### 🎉 New features | ||
- Add bare workflow support to `getExpoPushTokenAsync`. ([#12465](https://github.com/expo/expo/pull/12465) by [@EvanBacon](https://github.com/EvanBacon)) | ||
- [plugin] Refactor imports ([#13029](https://github.com/expo/expo/pull/13029) by [@EvanBacon](https://github.com/EvanBacon)) | ||
- Add support for custom notification sounds when using EAS Build. ([#12782](https://github.com/expo/expo/pull/12782) by [@cruzach](https://github.com/cruzach)) | ||
- Added ability to respond to remote notifications received while the app is backgrounded. ([#13130](https://github.com/expo/expo/pull/13130) by [@cruzach](https://github.com/cruzach)) | ||
### 🐛 Bug fixes | ||
- Enable kotlin in all modules. ([#12716](https://github.com/expo/expo/pull/12716) by [@wschurman](https://github.com/wschurman)) | ||
- Add new manifest2 field and make existing field optional. ([#12817](https://github.com/expo/expo/pull/12817) by [@wschurman](https://github.com/wschurman)) | ||
- Use originalFullName instead of currentFullName ([#12953](https://github.com/expo/expo/pull/12953)) by [@wschurman](https://github.com/wschurman)) | ||
### 💡 Others | ||
- Migrated from `unimodules-permissions-interface` to `expo-modules-core`. ([#12961](https://github.com/expo/expo/pull/12961) by [@tsapeta](https://github.com/tsapeta)) | ||
- Refactored uuid imports to v7 style. ([#13037](https://github.com/expo/expo/pull/13037) by [@giautm](https://github.com/giautm)) | ||
## 0.11.5 — 2021-04-13 | ||
@@ -19,0 +34,0 @@ |
{ | ||
"name": "expo-notifications", | ||
"version": "0.11.6", | ||
"version": "0.12.0", | ||
"description": "Notifications module", | ||
@@ -41,10 +41,9 @@ "main": "build/index.js", | ||
"@unimodules/core": "*", | ||
"unimodules-task-manager-interface": "*", | ||
"expo-application": "^2.1.0", | ||
"expo-constants": ">=9.3.3 <11.0.0", | ||
"unimodules-permissions-interface": "*" | ||
"expo-constants": ">=9.3.3 <11.0.0" | ||
}, | ||
"dependencies": { | ||
"@expo/config-plugins": "^1.0.18", | ||
"@expo/config-plugins": "^2.0.0", | ||
"@expo/image-utils": "^0.3.10", | ||
"fs-extra": "^9.0.1", | ||
"@ide/backoff": "^1.0.0", | ||
@@ -54,9 +53,9 @@ "abort-controller": "^3.0.0", | ||
"badgin": "^1.1.5", | ||
"expo-application": "~3.1.2", | ||
"expo-constants": "10.1.3", | ||
"uuid": "^3.4.0", | ||
"unimodules-permissions-interface": "~6.1.0" | ||
"expo-application": "~3.2.0", | ||
"expo-constants": "11.0.0", | ||
"expo-modules-core": "~0.1.1", | ||
"fs-extra": "^9.0.1", | ||
"uuid": "^3.4.0" | ||
}, | ||
"devDependencies": { | ||
"@types/fs-extra": "^9.0.6", | ||
"@types/node-fetch": "^2.5.7", | ||
@@ -68,3 +67,3 @@ "@types/uuid": "^3.4.7", | ||
}, | ||
"gitHead": "552e4360dc8adacb2b3173051dc50d7fb6afbf84" | ||
"gitHead": "b33f5e224578564c3e4b1b467f258cc119b3b786" | ||
} |
import { ConfigPlugin } from '@expo/config-plugins'; | ||
declare const _default: ConfigPlugin<void>; | ||
export declare type NotificationsPluginProps = { | ||
/** | ||
* (Android only) Local path to an image to use as the icon for push notifications. | ||
* 96x96 all-white png with transparency. We recommend following | ||
* [Google's design guidelines](https://material.io/design/iconography/product-icons.html#design-principles). | ||
*/ | ||
icon?: string; | ||
/** | ||
* (Android only) Tint color for the push notification image when it appears in the notification tray. | ||
* @default '#ffffff' | ||
*/ | ||
color?: string; | ||
/** | ||
* Array of local paths to sound files (.wav recommended) that can be used as custom notification sounds. | ||
*/ | ||
sounds?: string[]; | ||
/** | ||
* (iOS only) Environment of the app: either 'development' or 'production'. Defaults to 'development'. | ||
* @default 'development' | ||
*/ | ||
mode?: 'development' | 'production'; | ||
}; | ||
declare const _default: ConfigPlugin<void | NotificationsPluginProps>; | ||
export default _default; |
@@ -7,7 +7,7 @@ "use strict"; | ||
const pkg = require('expo-notifications/package.json'); | ||
const withNotifications = config => { | ||
config = withNotificationsAndroid_1.withNotificationsAndroid(config); | ||
config = withNotificationsIOS_1.withNotificationsIOS(config, { mode: 'development' }); | ||
const withNotifications = (config, props) => { | ||
config = withNotificationsAndroid_1.withNotificationsAndroid(config, props || {}); | ||
config = withNotificationsIOS_1.withNotificationsIOS(config, props || {}); | ||
return config; | ||
}; | ||
exports.default = config_plugins_1.createRunOncePlugin(withNotifications, pkg.name, pkg.version); |
@@ -1,3 +0,11 @@ | ||
import { AndroidConfig, ConfigPlugin } from '@expo/config-plugins'; | ||
import { ConfigPlugin } from '@expo/config-plugins'; | ||
import { ExpoConfig } from '@expo/config-types'; | ||
import { NotificationsPluginProps } from './withNotifications'; | ||
declare type DPIString = 'mdpi' | 'hdpi' | 'xhdpi' | 'xxhdpi' | 'xxxhdpi'; | ||
declare type dpiMap = Record<DPIString, { | ||
folderName: string; | ||
scale: number; | ||
}>; | ||
export declare const ANDROID_RES_PATH = "android/app/src/main/res/"; | ||
export declare const dpiValues: dpiMap; | ||
export declare const META_DATA_NOTIFICATION_ICON = "expo.modules.notifications.default_notification_icon"; | ||
@@ -9,14 +17,27 @@ export declare const META_DATA_NOTIFICATION_ICON_COLOR = "expo.modules.notifications.default_notification_color"; | ||
export declare const NOTIFICATION_ICON_COLOR_RESOURCE: string; | ||
export declare const withNotificationIcons: ConfigPlugin; | ||
export declare const withNotificationIconColor: ConfigPlugin; | ||
export declare const withNotificationManifest: ConfigPlugin<void>; | ||
export declare const withNotificationIcons: ConfigPlugin<{ | ||
icon: string | null; | ||
}>; | ||
export declare const withNotificationIconColor: ConfigPlugin<{ | ||
color: string | null; | ||
}>; | ||
export declare const withNotificationManifest: ConfigPlugin<{ | ||
icon: string | null; | ||
color: string | null; | ||
}>; | ||
export declare const withNotificationSounds: ConfigPlugin<{ | ||
sounds: string[]; | ||
}>; | ||
export declare function getNotificationIcon(config: ExpoConfig): string | null; | ||
export declare function getNotificationColor(config: ExpoConfig): string | null; | ||
/** | ||
* Applies configuration for expo-notifications, including | ||
* the notification icon and notification color. | ||
* Applies notification icon configuration for expo-notifications | ||
*/ | ||
export declare function setNotificationIconAsync(config: ExpoConfig, projectRoot: string): Promise<void>; | ||
export declare function setNotificationConfigAsync(config: ExpoConfig, manifest: AndroidConfig.Manifest.AndroidManifest): Promise<AndroidConfig.Manifest.AndroidManifest>; | ||
export declare function setNotificationIconColorAsync(config: ExpoConfig, projectRoot: string): Promise<void>; | ||
export declare const withNotificationsAndroid: ConfigPlugin; | ||
export declare function setNotificationIconAsync(projectRoot: string, icon: string | null): Promise<void>; | ||
export declare function setNotificationIconColorAsync(projectRoot: string, color: string | null): Promise<void>; | ||
/** | ||
* Save sound files to `<project-root>/android/app/src/main/res/raw` | ||
*/ | ||
export declare function setNotificationSounds(projectRoot: string, sounds: string[]): void; | ||
export declare const withNotificationsAndroid: ConfigPlugin<NotificationsPluginProps>; | ||
export {}; |
"use strict"; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.withNotificationsAndroid = exports.setNotificationIconColorAsync = exports.setNotificationConfigAsync = exports.setNotificationIconAsync = exports.getNotificationColor = exports.getNotificationIcon = exports.withNotificationManifest = exports.withNotificationIconColor = exports.withNotificationIcons = exports.NOTIFICATION_ICON_COLOR_RESOURCE = exports.NOTIFICATION_ICON_COLOR = exports.NOTIFICATION_ICON_RESOURCE = exports.NOTIFICATION_ICON = exports.META_DATA_NOTIFICATION_ICON_COLOR = exports.META_DATA_NOTIFICATION_ICON = void 0; | ||
exports.withNotificationsAndroid = exports.setNotificationSounds = exports.setNotificationIconColorAsync = exports.setNotificationIconAsync = exports.getNotificationColor = exports.getNotificationIcon = exports.withNotificationSounds = exports.withNotificationManifest = exports.withNotificationIconColor = exports.withNotificationIcons = exports.NOTIFICATION_ICON_COLOR_RESOURCE = exports.NOTIFICATION_ICON_COLOR = exports.NOTIFICATION_ICON_RESOURCE = exports.NOTIFICATION_ICON = exports.META_DATA_NOTIFICATION_ICON_COLOR = exports.META_DATA_NOTIFICATION_ICON = exports.dpiValues = exports.ANDROID_RES_PATH = void 0; | ||
const config_plugins_1 = require("@expo/config-plugins"); | ||
const Resources_1 = require("@expo/config-plugins/build/android/Resources"); | ||
const android_plugins_1 = require("@expo/config-plugins/build/plugins/android-plugins"); | ||
const XML_1 = require("@expo/config-plugins/build/utils/XML"); | ||
const image_utils_1 = require("@expo/image-utils"); | ||
const fs_extra_1 = __importDefault(require("fs-extra")); | ||
const path_1 = __importDefault(require("path")); | ||
const fs_1 = require("fs"); | ||
const path_1 = require("path"); | ||
const { buildResourceItem, readResourcesXMLAsync } = config_plugins_1.AndroidConfig.Resources; | ||
const { writeXMLAsync } = config_plugins_1.XML; | ||
const { Colors } = config_plugins_1.AndroidConfig; | ||
const { ANDROID_RES_PATH, dpiValues } = config_plugins_1.AndroidConfig.Icon; | ||
exports.ANDROID_RES_PATH = 'android/app/src/main/res/'; | ||
exports.dpiValues = { | ||
mdpi: { folderName: 'mipmap-mdpi', scale: 1 }, | ||
hdpi: { folderName: 'mipmap-hdpi', scale: 1.5 }, | ||
xhdpi: { folderName: 'mipmap-xhdpi', scale: 2 }, | ||
xxhdpi: { folderName: 'mipmap-xxhdpi', scale: 3 }, | ||
xxxhdpi: { folderName: 'mipmap-xxxhdpi', scale: 4 }, | ||
}; | ||
const { addMetaDataItemToMainApplication, getMainApplicationOrThrow, removeMetaDataItemFromMainApplication, } = config_plugins_1.AndroidConfig.Manifest; | ||
const BASELINE_PIXEL_SIZE = 24; | ||
const ERROR_MSG_PREFIX = 'An error occurred while configuring Android notifications. '; | ||
exports.META_DATA_NOTIFICATION_ICON = 'expo.modules.notifications.default_notification_icon'; | ||
@@ -24,7 +28,9 @@ exports.META_DATA_NOTIFICATION_ICON_COLOR = 'expo.modules.notifications.default_notification_color'; | ||
exports.NOTIFICATION_ICON_COLOR_RESOURCE = `@color/${exports.NOTIFICATION_ICON_COLOR}`; | ||
exports.withNotificationIcons = config => { | ||
exports.withNotificationIcons = (config, { icon }) => { | ||
// If no icon provided in the config plugin props, fallback to value from app.json | ||
icon = icon || getNotificationIcon(config); | ||
return config_plugins_1.withDangerousMod(config, [ | ||
'android', | ||
async (config) => { | ||
await setNotificationIconAsync(config, config.modRequest.projectRoot); | ||
await setNotificationIconAsync(config.modRequest.projectRoot, icon); | ||
return config; | ||
@@ -34,7 +40,9 @@ }, | ||
}; | ||
exports.withNotificationIconColor = config => { | ||
exports.withNotificationIconColor = (config, { color }) => { | ||
// If no color provided in the config plugin props, fallback to value from app.json | ||
color = color || getNotificationColor(config); | ||
return config_plugins_1.withDangerousMod(config, [ | ||
'android', | ||
async (config) => { | ||
await setNotificationIconColorAsync(config, config.modRequest.projectRoot); | ||
await setNotificationIconColorAsync(config.modRequest.projectRoot, color); | ||
return config; | ||
@@ -44,3 +52,20 @@ }, | ||
}; | ||
exports.withNotificationManifest = android_plugins_1.createAndroidManifestPlugin(setNotificationConfigAsync, 'withNotificationManifest'); | ||
exports.withNotificationManifest = (config, { icon, color }) => { | ||
// If no icon or color provided in the config plugin props, fallback to value from app.json | ||
icon = icon || getNotificationIcon(config); | ||
color = color || getNotificationColor(config); | ||
return config_plugins_1.withAndroidManifest(config, config => { | ||
config.modResults = setNotificationConfig({ icon, color }, config.modResults); | ||
return config; | ||
}); | ||
}; | ||
exports.withNotificationSounds = (config, { sounds }) => { | ||
return config_plugins_1.withDangerousMod(config, [ | ||
'android', | ||
config => { | ||
setNotificationSounds(config.modRequest.projectRoot, sounds); | ||
return config; | ||
}, | ||
]); | ||
}; | ||
function getNotificationIcon(config) { | ||
@@ -57,7 +82,5 @@ var _a; | ||
/** | ||
* Applies configuration for expo-notifications, including | ||
* the notification icon and notification color. | ||
* Applies notification icon configuration for expo-notifications | ||
*/ | ||
async function setNotificationIconAsync(config, projectRoot) { | ||
const icon = getNotificationIcon(config); | ||
async function setNotificationIconAsync(projectRoot, icon) { | ||
if (icon) { | ||
@@ -67,11 +90,9 @@ await writeNotificationIconImageFilesAsync(icon, projectRoot); | ||
else { | ||
await removeNotificationIconImageFilesAsync(projectRoot); | ||
removeNotificationIconImageFiles(projectRoot); | ||
} | ||
} | ||
exports.setNotificationIconAsync = setNotificationIconAsync; | ||
async function setNotificationConfigAsync(config, manifest) { | ||
const icon = getNotificationIcon(config); | ||
const color = getNotificationColor(config); | ||
function setNotificationConfig(props, manifest) { | ||
const mainApplication = getMainApplicationOrThrow(manifest); | ||
if (icon) { | ||
if (props.icon) { | ||
addMetaDataItemToMainApplication(mainApplication, exports.META_DATA_NOTIFICATION_ICON, exports.NOTIFICATION_ICON_RESOURCE, 'resource'); | ||
@@ -82,3 +103,3 @@ } | ||
} | ||
if (color) { | ||
if (props.color) { | ||
addMetaDataItemToMainApplication(mainApplication, exports.META_DATA_NOTIFICATION_ICON_COLOR, exports.NOTIFICATION_ICON_COLOR_RESOURCE, 'resource'); | ||
@@ -91,9 +112,7 @@ } | ||
} | ||
exports.setNotificationConfigAsync = setNotificationConfigAsync; | ||
async function setNotificationIconColorAsync(config, projectRoot) { | ||
const color = getNotificationColor(config); | ||
async function setNotificationIconColorAsync(projectRoot, color) { | ||
const colorsXmlPath = await Colors.getProjectColorsXMLPathAsync(projectRoot); | ||
let colorsJson = await Resources_1.readResourcesXMLAsync({ path: colorsXmlPath }); | ||
let colorsJson = await readResourcesXMLAsync({ path: colorsXmlPath }); | ||
if (color) { | ||
const colorItemToAdd = Resources_1.buildResourceItem({ name: exports.NOTIFICATION_ICON_COLOR, value: color }); | ||
const colorItemToAdd = buildResourceItem({ name: exports.NOTIFICATION_ICON_COLOR, value: color }); | ||
colorsJson = Colors.setColorItem(colorItemToAdd, colorsJson); | ||
@@ -104,10 +123,12 @@ } | ||
} | ||
await XML_1.writeXMLAsync({ path: colorsXmlPath, xml: colorsJson }); | ||
await writeXMLAsync({ path: colorsXmlPath, xml: colorsJson }); | ||
} | ||
exports.setNotificationIconColorAsync = setNotificationIconColorAsync; | ||
async function writeNotificationIconImageFilesAsync(icon, projectRoot) { | ||
await Promise.all(Object.values(dpiValues).map(async ({ folderName, scale }) => { | ||
await Promise.all(Object.values(exports.dpiValues).map(async ({ folderName, scale }) => { | ||
const drawableFolderName = folderName.replace('mipmap', 'drawable'); | ||
const dpiFolderPath = path_1.default.resolve(projectRoot, ANDROID_RES_PATH, drawableFolderName); | ||
await fs_extra_1.default.ensureDir(dpiFolderPath); | ||
const dpiFolderPath = path_1.resolve(projectRoot, exports.ANDROID_RES_PATH, drawableFolderName); | ||
if (!fs_1.existsSync(dpiFolderPath)) { | ||
fs_1.mkdirSync(dpiFolderPath, { recursive: true }); | ||
} | ||
const iconSizePx = BASELINE_PIXEL_SIZE * scale; | ||
@@ -122,21 +143,56 @@ try { | ||
})).source; | ||
await fs_extra_1.default.writeFile(path_1.default.resolve(dpiFolderPath, exports.NOTIFICATION_ICON + '.png'), resizedIcon); | ||
fs_1.writeFileSync(path_1.resolve(dpiFolderPath, exports.NOTIFICATION_ICON + '.png'), resizedIcon); | ||
} | ||
catch (e) { | ||
throw new Error('Encountered an issue resizing Android notification icon: ' + e); | ||
throw new Error(ERROR_MSG_PREFIX + 'Encountered an issue resizing Android notification icon: ' + e); | ||
} | ||
})); | ||
} | ||
async function removeNotificationIconImageFilesAsync(projectRoot) { | ||
await Promise.all(Object.values(dpiValues).map(async ({ folderName }) => { | ||
function removeNotificationIconImageFiles(projectRoot) { | ||
Object.values(exports.dpiValues).forEach(async ({ folderName }) => { | ||
const drawableFolderName = folderName.replace('mipmap', 'drawable'); | ||
const dpiFolderPath = path_1.default.resolve(projectRoot, ANDROID_RES_PATH, drawableFolderName); | ||
await fs_extra_1.default.remove(path_1.default.resolve(dpiFolderPath, exports.NOTIFICATION_ICON + '.png')); | ||
})); | ||
const dpiFolderPath = path_1.resolve(projectRoot, exports.ANDROID_RES_PATH, drawableFolderName); | ||
fs_1.unlinkSync(path_1.resolve(dpiFolderPath, exports.NOTIFICATION_ICON + '.png')); | ||
}); | ||
} | ||
exports.withNotificationsAndroid = config => { | ||
config = exports.withNotificationIconColor(config); | ||
config = exports.withNotificationIcons(config); | ||
config = exports.withNotificationManifest(config); | ||
/** | ||
* Save sound files to `<project-root>/android/app/src/main/res/raw` | ||
*/ | ||
function setNotificationSounds(projectRoot, sounds) { | ||
if (!Array.isArray(sounds)) { | ||
throw new Error(ERROR_MSG_PREFIX + | ||
`Must provide an array of sound files in your app config, found ${typeof sounds}.`); | ||
} | ||
for (const soundFileRelativePath of sounds) { | ||
writeNotificationSoundFile(soundFileRelativePath, projectRoot); | ||
} | ||
} | ||
exports.setNotificationSounds = setNotificationSounds; | ||
/** | ||
* Copies the input file to the `<project-root>/android/app/src/main/res/raw` directory if | ||
* there isn't already an existing file under that name. | ||
*/ | ||
function writeNotificationSoundFile(soundFileRelativePath, projectRoot) { | ||
const rawResourcesPath = path_1.resolve(projectRoot, exports.ANDROID_RES_PATH, 'raw'); | ||
const inputFilename = path_1.basename(soundFileRelativePath); | ||
if (inputFilename) { | ||
try { | ||
const sourceFilepath = path_1.resolve(projectRoot, soundFileRelativePath); | ||
const destinationFilepath = path_1.resolve(rawResourcesPath, inputFilename); | ||
if (!fs_1.existsSync(rawResourcesPath)) { | ||
fs_1.mkdirSync(rawResourcesPath, { recursive: true }); | ||
} | ||
fs_1.copyFileSync(sourceFilepath, destinationFilepath); | ||
} | ||
catch (e) { | ||
throw new Error(ERROR_MSG_PREFIX + 'Encountered an issue copying Android notification sounds: ' + e); | ||
} | ||
} | ||
} | ||
exports.withNotificationsAndroid = (config, { icon = null, color = null, sounds = [] }) => { | ||
config = exports.withNotificationIconColor(config, { color }); | ||
config = exports.withNotificationIcons(config, { icon }); | ||
config = exports.withNotificationManifest(config, { icon, color }); | ||
config = exports.withNotificationSounds(config, { sounds }); | ||
return config; | ||
}; |
@@ -1,4 +0,14 @@ | ||
import { ConfigPlugin } from '@expo/config-plugins'; | ||
export declare const withNotificationsIOS: ConfigPlugin<{ | ||
mode: 'production' | 'development'; | ||
import { ConfigPlugin, XcodeProject } from '@expo/config-plugins'; | ||
import { NotificationsPluginProps } from './withNotifications'; | ||
export declare const withNotificationsIOS: ConfigPlugin<NotificationsPluginProps>; | ||
export declare const withNotificationSounds: ConfigPlugin<{ | ||
sounds: string[]; | ||
}>; | ||
/** | ||
* Save sound files to the Xcode project root and add them to the Xcode project. | ||
*/ | ||
export declare function setNotificationSounds(projectRoot: string, { sounds, project, projectName, }: { | ||
sounds: string[]; | ||
project: XcodeProject; | ||
projectName: string | undefined; | ||
}): XcodeProject; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.withNotificationsIOS = void 0; | ||
exports.setNotificationSounds = exports.withNotificationSounds = exports.withNotificationsIOS = void 0; | ||
const config_plugins_1 = require("@expo/config-plugins"); | ||
exports.withNotificationsIOS = (config, { mode }) => { | ||
return config_plugins_1.withEntitlementsPlist(config, config => { | ||
const fs_1 = require("fs"); | ||
const path_1 = require("path"); | ||
const ERROR_MSG_PREFIX = 'An error occurred while configuring iOS notifications. '; | ||
exports.withNotificationsIOS = (config, { mode = 'development', sounds = [] }) => { | ||
config = config_plugins_1.withEntitlementsPlist(config, config => { | ||
config.modResults['aps-environment'] = mode; | ||
return config; | ||
}); | ||
config = exports.withNotificationSounds(config, { sounds }); | ||
return config; | ||
}; | ||
exports.withNotificationSounds = (config, { sounds }) => { | ||
return config_plugins_1.withXcodeProject(config, config => { | ||
setNotificationSounds(config.modRequest.projectRoot, { | ||
sounds, | ||
project: config.modResults, | ||
projectName: config.modRequest.projectName, | ||
}); | ||
return config; | ||
}); | ||
}; | ||
/** | ||
* Save sound files to the Xcode project root and add them to the Xcode project. | ||
*/ | ||
function setNotificationSounds(projectRoot, { sounds, project, projectName, }) { | ||
if (!projectName) { | ||
throw new Error(ERROR_MSG_PREFIX + `Unable to find iOS project name.`); | ||
} | ||
if (!Array.isArray(sounds)) { | ||
throw new Error(ERROR_MSG_PREFIX + | ||
`Must provide an array of sound files in your app config, found ${typeof sounds}.`); | ||
} | ||
const sourceRoot = config_plugins_1.IOSConfig.Paths.getSourceRoot(projectRoot); | ||
for (const soundFileRelativePath of sounds) { | ||
const fileName = path_1.basename(soundFileRelativePath); | ||
const sourceFilepath = path_1.resolve(projectRoot, soundFileRelativePath); | ||
const destinationFilepath = path_1.resolve(sourceRoot, fileName); | ||
// Since it's possible that the filename is the same, but the | ||
// file itself id different, let's copy it regardless | ||
fs_1.copyFileSync(sourceFilepath, destinationFilepath); | ||
if (!project.hasFile(`${projectName}/${fileName}`)) { | ||
project = config_plugins_1.IOSConfig.XcodeUtils.addResourceFileToGroup({ | ||
filepath: `${projectName}/${fileName}`, | ||
groupName: projectName, | ||
isBuildFile: true, | ||
project, | ||
}); | ||
} | ||
} | ||
return project; | ||
} | ||
exports.setNotificationSounds = setNotificationSounds; |
@@ -8,5 +8,28 @@ import { ConfigPlugin, createRunOncePlugin } from '@expo/config-plugins'; | ||
const withNotifications: ConfigPlugin = config => { | ||
config = withNotificationsAndroid(config); | ||
config = withNotificationsIOS(config, { mode: 'development' }); | ||
export type NotificationsPluginProps = { | ||
/** | ||
* (Android only) Local path to an image to use as the icon for push notifications. | ||
* 96x96 all-white png with transparency. We recommend following | ||
* [Google's design guidelines](https://material.io/design/iconography/product-icons.html#design-principles). | ||
*/ | ||
icon?: string; | ||
/** | ||
* (Android only) Tint color for the push notification image when it appears in the notification tray. | ||
* @default '#ffffff' | ||
*/ | ||
color?: string; | ||
/** | ||
* Array of local paths to sound files (.wav recommended) that can be used as custom notification sounds. | ||
*/ | ||
sounds?: string[]; | ||
/** | ||
* (iOS only) Environment of the app: either 'development' or 'production'. Defaults to 'development'. | ||
* @default 'development' | ||
*/ | ||
mode?: 'development' | 'production'; | ||
}; | ||
const withNotifications: ConfigPlugin<NotificationsPluginProps | void> = (config, props) => { | ||
config = withNotificationsAndroid(config, props || {}); | ||
config = withNotificationsIOS(config, props || {}); | ||
return config; | ||
@@ -13,0 +36,0 @@ }; |
@@ -1,15 +0,30 @@ | ||
import { AndroidConfig, ConfigPlugin, withDangerousMod } from '@expo/config-plugins'; | ||
import { | ||
buildResourceItem, | ||
readResourcesXMLAsync, | ||
} from '@expo/config-plugins/build/android/Resources'; | ||
import { createAndroidManifestPlugin } from '@expo/config-plugins/build/plugins/android-plugins'; | ||
import { writeXMLAsync } from '@expo/config-plugins/build/utils/XML'; | ||
AndroidConfig, | ||
ConfigPlugin, | ||
withDangerousMod, | ||
withAndroidManifest, | ||
XML, | ||
} from '@expo/config-plugins'; | ||
import { ExpoConfig } from '@expo/config-types'; | ||
import { generateImageAsync } from '@expo/image-utils'; | ||
import fs from 'fs-extra'; | ||
import path from 'path'; | ||
import { writeFileSync, unlinkSync, copyFileSync, existsSync, mkdirSync } from 'fs'; | ||
import { basename, resolve } from 'path'; | ||
import { NotificationsPluginProps } from './withNotifications'; | ||
const { buildResourceItem, readResourcesXMLAsync } = AndroidConfig.Resources; | ||
const { writeXMLAsync } = XML; | ||
const { Colors } = AndroidConfig; | ||
const { ANDROID_RES_PATH, dpiValues } = AndroidConfig.Icon; | ||
type DPIString = 'mdpi' | 'hdpi' | 'xhdpi' | 'xxhdpi' | 'xxxhdpi'; | ||
type dpiMap = Record<DPIString, { folderName: string; scale: number }>; | ||
export const ANDROID_RES_PATH = 'android/app/src/main/res/'; | ||
export const dpiValues: dpiMap = { | ||
mdpi: { folderName: 'mipmap-mdpi', scale: 1 }, | ||
hdpi: { folderName: 'mipmap-hdpi', scale: 1.5 }, | ||
xhdpi: { folderName: 'mipmap-xhdpi', scale: 2 }, | ||
xxhdpi: { folderName: 'mipmap-xxhdpi', scale: 3 }, | ||
xxxhdpi: { folderName: 'mipmap-xxxhdpi', scale: 4 }, | ||
}; | ||
const { | ||
@@ -21,2 +36,3 @@ addMetaDataItemToMainApplication, | ||
const BASELINE_PIXEL_SIZE = 24; | ||
const ERROR_MSG_PREFIX = 'An error occurred while configuring Android notifications. '; | ||
export const META_DATA_NOTIFICATION_ICON = 'expo.modules.notifications.default_notification_icon'; | ||
@@ -30,7 +46,9 @@ export const META_DATA_NOTIFICATION_ICON_COLOR = | ||
export const withNotificationIcons: ConfigPlugin = config => { | ||
export const withNotificationIcons: ConfigPlugin<{ icon: string | null }> = (config, { icon }) => { | ||
// If no icon provided in the config plugin props, fallback to value from app.json | ||
icon = icon || getNotificationIcon(config); | ||
return withDangerousMod(config, [ | ||
'android', | ||
async config => { | ||
await setNotificationIconAsync(config, config.modRequest.projectRoot); | ||
await setNotificationIconAsync(config.modRequest.projectRoot, icon); | ||
return config; | ||
@@ -41,7 +59,12 @@ }, | ||
export const withNotificationIconColor: ConfigPlugin = config => { | ||
export const withNotificationIconColor: ConfigPlugin<{ color: string | null }> = ( | ||
config, | ||
{ color } | ||
) => { | ||
// If no color provided in the config plugin props, fallback to value from app.json | ||
color = color || getNotificationColor(config); | ||
return withDangerousMod(config, [ | ||
'android', | ||
async config => { | ||
await setNotificationIconColorAsync(config, config.modRequest.projectRoot); | ||
await setNotificationIconColorAsync(config.modRequest.projectRoot, color); | ||
return config; | ||
@@ -52,7 +75,25 @@ }, | ||
export const withNotificationManifest = createAndroidManifestPlugin( | ||
setNotificationConfigAsync, | ||
'withNotificationManifest' | ||
); | ||
export const withNotificationManifest: ConfigPlugin<{ | ||
icon: string | null; | ||
color: string | null; | ||
}> = (config, { icon, color }) => { | ||
// If no icon or color provided in the config plugin props, fallback to value from app.json | ||
icon = icon || getNotificationIcon(config); | ||
color = color || getNotificationColor(config); | ||
return withAndroidManifest(config, config => { | ||
config.modResults = setNotificationConfig({ icon, color }, config.modResults); | ||
return config; | ||
}); | ||
}; | ||
export const withNotificationSounds: ConfigPlugin<{ sounds: string[] }> = (config, { sounds }) => { | ||
return withDangerousMod(config, [ | ||
'android', | ||
config => { | ||
setNotificationSounds(config.modRequest.projectRoot, sounds); | ||
return config; | ||
}, | ||
]); | ||
}; | ||
export function getNotificationIcon(config: ExpoConfig) { | ||
@@ -67,22 +108,18 @@ return config.notification?.icon || null; | ||
/** | ||
* Applies configuration for expo-notifications, including | ||
* the notification icon and notification color. | ||
* Applies notification icon configuration for expo-notifications | ||
*/ | ||
export async function setNotificationIconAsync(config: ExpoConfig, projectRoot: string) { | ||
const icon = getNotificationIcon(config); | ||
export async function setNotificationIconAsync(projectRoot: string, icon: string | null) { | ||
if (icon) { | ||
await writeNotificationIconImageFilesAsync(icon, projectRoot); | ||
} else { | ||
await removeNotificationIconImageFilesAsync(projectRoot); | ||
removeNotificationIconImageFiles(projectRoot); | ||
} | ||
} | ||
export async function setNotificationConfigAsync( | ||
config: ExpoConfig, | ||
function setNotificationConfig( | ||
props: { icon: string | null; color: string | null }, | ||
manifest: AndroidConfig.Manifest.AndroidManifest | ||
) { | ||
const icon = getNotificationIcon(config); | ||
const color = getNotificationColor(config); | ||
const mainApplication = getMainApplicationOrThrow(manifest); | ||
if (icon) { | ||
if (props.icon) { | ||
addMetaDataItemToMainApplication( | ||
@@ -97,3 +134,3 @@ mainApplication, | ||
} | ||
if (color) { | ||
if (props.color) { | ||
addMetaDataItemToMainApplication( | ||
@@ -111,4 +148,3 @@ mainApplication, | ||
export async function setNotificationIconColorAsync(config: ExpoConfig, projectRoot: string) { | ||
const color = getNotificationColor(config); | ||
export async function setNotificationIconColorAsync(projectRoot: string, color: string | null) { | ||
const colorsXmlPath = await Colors.getProjectColorsXMLPathAsync(projectRoot); | ||
@@ -129,4 +165,6 @@ let colorsJson = await readResourcesXMLAsync({ path: colorsXmlPath }); | ||
const drawableFolderName = folderName.replace('mipmap', 'drawable'); | ||
const dpiFolderPath = path.resolve(projectRoot, ANDROID_RES_PATH, drawableFolderName); | ||
await fs.ensureDir(dpiFolderPath); | ||
const dpiFolderPath = resolve(projectRoot, ANDROID_RES_PATH, drawableFolderName); | ||
if (!existsSync(dpiFolderPath)) { | ||
mkdirSync(dpiFolderPath, { recursive: true }); | ||
} | ||
const iconSizePx = BASELINE_PIXEL_SIZE * scale; | ||
@@ -147,5 +185,7 @@ | ||
).source; | ||
await fs.writeFile(path.resolve(dpiFolderPath, NOTIFICATION_ICON + '.png'), resizedIcon); | ||
writeFileSync(resolve(dpiFolderPath, NOTIFICATION_ICON + '.png'), resizedIcon); | ||
} catch (e) { | ||
throw new Error('Encountered an issue resizing Android notification icon: ' + e); | ||
throw new Error( | ||
ERROR_MSG_PREFIX + 'Encountered an issue resizing Android notification icon: ' + e | ||
); | ||
} | ||
@@ -156,17 +196,58 @@ }) | ||
async function removeNotificationIconImageFilesAsync(projectRoot: string) { | ||
await Promise.all( | ||
Object.values(dpiValues).map(async ({ folderName }) => { | ||
const drawableFolderName = folderName.replace('mipmap', 'drawable'); | ||
const dpiFolderPath = path.resolve(projectRoot, ANDROID_RES_PATH, drawableFolderName); | ||
await fs.remove(path.resolve(dpiFolderPath, NOTIFICATION_ICON + '.png')); | ||
}) | ||
); | ||
function removeNotificationIconImageFiles(projectRoot: string) { | ||
Object.values(dpiValues).forEach(async ({ folderName }) => { | ||
const drawableFolderName = folderName.replace('mipmap', 'drawable'); | ||
const dpiFolderPath = resolve(projectRoot, ANDROID_RES_PATH, drawableFolderName); | ||
unlinkSync(resolve(dpiFolderPath, NOTIFICATION_ICON + '.png')); | ||
}); | ||
} | ||
export const withNotificationsAndroid: ConfigPlugin = config => { | ||
config = withNotificationIconColor(config); | ||
config = withNotificationIcons(config); | ||
config = withNotificationManifest(config); | ||
/** | ||
* Save sound files to `<project-root>/android/app/src/main/res/raw` | ||
*/ | ||
export function setNotificationSounds(projectRoot: string, sounds: string[]) { | ||
if (!Array.isArray(sounds)) { | ||
throw new Error( | ||
ERROR_MSG_PREFIX + | ||
`Must provide an array of sound files in your app config, found ${typeof sounds}.` | ||
); | ||
} | ||
for (const soundFileRelativePath of sounds) { | ||
writeNotificationSoundFile(soundFileRelativePath, projectRoot); | ||
} | ||
} | ||
/** | ||
* Copies the input file to the `<project-root>/android/app/src/main/res/raw` directory if | ||
* there isn't already an existing file under that name. | ||
*/ | ||
function writeNotificationSoundFile(soundFileRelativePath: string, projectRoot: string) { | ||
const rawResourcesPath = resolve(projectRoot, ANDROID_RES_PATH, 'raw'); | ||
const inputFilename = basename(soundFileRelativePath); | ||
if (inputFilename) { | ||
try { | ||
const sourceFilepath = resolve(projectRoot, soundFileRelativePath); | ||
const destinationFilepath = resolve(rawResourcesPath, inputFilename); | ||
if (!existsSync(rawResourcesPath)) { | ||
mkdirSync(rawResourcesPath, { recursive: true }); | ||
} | ||
copyFileSync(sourceFilepath, destinationFilepath); | ||
} catch (e) { | ||
throw new Error( | ||
ERROR_MSG_PREFIX + 'Encountered an issue copying Android notification sounds: ' + e | ||
); | ||
} | ||
} | ||
} | ||
export const withNotificationsAndroid: ConfigPlugin<NotificationsPluginProps> = ( | ||
config, | ||
{ icon = null, color = null, sounds = [] } | ||
) => { | ||
config = withNotificationIconColor(config, { color }); | ||
config = withNotificationIcons(config, { icon }); | ||
config = withNotificationManifest(config, { icon, color }); | ||
config = withNotificationSounds(config, { sounds }); | ||
return config; | ||
}; |
@@ -1,11 +0,78 @@ | ||
import { ConfigPlugin, withEntitlementsPlist } from '@expo/config-plugins'; | ||
import { | ||
ConfigPlugin, | ||
withEntitlementsPlist, | ||
IOSConfig, | ||
withXcodeProject, | ||
XcodeProject, | ||
} from '@expo/config-plugins'; | ||
import { copyFileSync } from 'fs'; | ||
import { basename, resolve } from 'path'; | ||
export const withNotificationsIOS: ConfigPlugin<{ mode: 'production' | 'development' }> = ( | ||
import { NotificationsPluginProps } from './withNotifications'; | ||
const ERROR_MSG_PREFIX = 'An error occurred while configuring iOS notifications. '; | ||
export const withNotificationsIOS: ConfigPlugin<NotificationsPluginProps> = ( | ||
config, | ||
{ mode } | ||
{ mode = 'development', sounds = [] } | ||
) => { | ||
return withEntitlementsPlist(config, config => { | ||
config = withEntitlementsPlist(config, config => { | ||
config.modResults['aps-environment'] = mode; | ||
return config; | ||
}); | ||
config = withNotificationSounds(config, { sounds }); | ||
return config; | ||
}; | ||
export const withNotificationSounds: ConfigPlugin<{ sounds: string[] }> = (config, { sounds }) => { | ||
return withXcodeProject(config, config => { | ||
setNotificationSounds(config.modRequest.projectRoot, { | ||
sounds, | ||
project: config.modResults, | ||
projectName: config.modRequest.projectName, | ||
}); | ||
return config; | ||
}); | ||
}; | ||
/** | ||
* Save sound files to the Xcode project root and add them to the Xcode project. | ||
*/ | ||
export function setNotificationSounds( | ||
projectRoot: string, | ||
{ | ||
sounds, | ||
project, | ||
projectName, | ||
}: { sounds: string[]; project: XcodeProject; projectName: string | undefined } | ||
): XcodeProject { | ||
if (!projectName) { | ||
throw new Error(ERROR_MSG_PREFIX + `Unable to find iOS project name.`); | ||
} | ||
if (!Array.isArray(sounds)) { | ||
throw new Error( | ||
ERROR_MSG_PREFIX + | ||
`Must provide an array of sound files in your app config, found ${typeof sounds}.` | ||
); | ||
} | ||
const sourceRoot = IOSConfig.Paths.getSourceRoot(projectRoot); | ||
for (const soundFileRelativePath of sounds) { | ||
const fileName = basename(soundFileRelativePath); | ||
const sourceFilepath = resolve(projectRoot, soundFileRelativePath); | ||
const destinationFilepath = resolve(sourceRoot, fileName); | ||
// Since it's possible that the filename is the same, but the | ||
// file itself id different, let's copy it regardless | ||
copyFileSync(sourceFilepath, destinationFilepath); | ||
if (!project.hasFile(`${projectName}/${fileName}`)) { | ||
project = IOSConfig.XcodeUtils.addResourceFileToGroup({ | ||
filepath: `${projectName}/${fileName}`, | ||
groupName: projectName, | ||
isBuildFile: true, | ||
project, | ||
}); | ||
} | ||
} | ||
return project; | ||
} |
@@ -103,3 +103,3 @@ import { CodedError, Platform, SyntheticPlatformEmitter } from '@unimodules/core'; | ||
// https://stackoverflow.com/a/35729334/2603230 | ||
const notificationIcon = (Constants.manifest.notification || {}).icon; | ||
const notificationIcon = (Constants.manifest?.notification || {}).icon; | ||
await registration.active.postMessage( | ||
@@ -106,0 +106,0 @@ JSON.stringify({ fromExpoWebClient: { notificationIcon } }) |
@@ -34,3 +34,3 @@ import { Platform, CodedError, UnavailabilityError } from '@unimodules/core'; | ||
const experienceId = | ||
options.experienceId || Constants.manifest?.currentFullName || Constants.manifest?.id; | ||
options.experienceId || Constants.manifest?.originalFullName || Constants.manifest?.id; | ||
@@ -37,0 +37,0 @@ if (!experienceId) { |
@@ -27,2 +27,4 @@ export { default as getDevicePushTokenAsync } from './getDevicePushTokenAsync'; | ||
export { setAutoServerRegistrationEnabledAsync } from './DevicePushTokenAutoRegistration.fx'; | ||
export { default as registerTaskAsync } from './registerTaskAsync'; | ||
export { default as unregisterTaskAsync } from './unregisterTaskAsync'; | ||
export * from './TokenEmitter'; | ||
@@ -29,0 +31,0 @@ export * from './NotificationsEmitter'; |
@@ -1,2 +0,2 @@ | ||
import { PermissionResponse } from 'unimodules-permissions-interface'; | ||
import { PermissionResponse } from 'expo-modules-core'; | ||
@@ -3,0 +3,0 @@ export enum IosAlertStyle { |
import { Platform } from '@unimodules/core'; | ||
import { PermissionStatus } from 'unimodules-permissions-interface'; | ||
import { PermissionStatus } from 'expo-modules-core'; | ||
@@ -4,0 +4,0 @@ import { |
import { UnavailabilityError } from '@unimodules/core'; | ||
import uuidv4 from 'uuid/v4'; | ||
import { v4 as uuidv4 } from 'uuid'; | ||
@@ -4,0 +4,0 @@ import NotificationPresenter from './NotificationPresenterModule'; |
import { Platform, UnavailabilityError } from '@unimodules/core'; | ||
import uuidv4 from 'uuid/v4'; | ||
import { v4 as uuidv4 } from 'uuid'; | ||
@@ -4,0 +4,0 @@ import NotificationScheduler from './NotificationScheduler'; |
import { CodedError } from '@unimodules/core'; | ||
import uuidv4 from 'uuid/v4'; | ||
import { v4 as uuidv4 } from 'uuid'; | ||
@@ -4,0 +4,0 @@ import { ServerRegistrationModule } from './ServerRegistrationModule.types'; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
3143758
5.51%5
-16.67%499
5.5%10298
5.71%1927
4.61%2
100%