├── .nvmrc ├── .watchmanconfig ├── example ├── .watchmanconfig ├── ios │ ├── .xcode.env.local │ ├── File.swift │ ├── ExpoAlarmModuleExample │ │ ├── Images.xcassets │ │ │ ├── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── AppDelegate.h │ │ ├── main.m │ │ ├── AppDelegate.mm │ │ ├── Info.plist │ │ └── LaunchScreen.storyboard │ ├── ExpoAlarmModuleExample-Bridging-Header.h │ ├── ExpoAlarmModuleExample.xcworkspace │ │ ├── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ │ └── contents.xcworkspacedata │ ├── .xcode.env │ ├── ExpoAlarmModuleExampleTests │ │ ├── Info.plist │ │ └── ExpoAlarmModuleExampleTests.m │ ├── Podfile │ └── ExpoAlarmModuleExample.xcodeproj │ │ ├── xcshareddata │ │ └── xcschemes │ │ │ └── ExpoAlarmModuleExample.xcscheme │ │ └── project.pbxproj ├── jest.config.js ├── .bundle │ └── config ├── app.json ├── android │ ├── app │ │ ├── debug.keystore │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── values │ │ │ │ │ │ ├── strings.xml │ │ │ │ │ │ └── styles.xml │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ └── drawable │ │ │ │ │ │ └── rn_edit_text_material.xml │ │ │ │ └── AndroidManifest.xml │ │ │ └── debug │ │ │ │ └── AndroidManifest.xml │ │ ├── proguard-rules.pro │ │ └── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── settings.gradle │ ├── build.gradle │ ├── gradle.properties │ ├── gradlew.bat │ └── gradlew ├── index.js ├── react-native.config.js ├── Gemfile ├── babel.config.js ├── package.json ├── metro.config.js ├── src │ └── App.tsx └── README.md ├── src ├── __tests__ │ └── index.test.tsx ├── utils.tsx ├── types │ └── Alarm.types.ts ├── models │ └── Alarm.tsx └── index.tsx ├── app.plugin.js ├── ios ├── bell.mp3 ├── ExpoAlarmModule-Bridging-Header.h ├── AlarmApplicationDelegate.swift ├── NotificationSchedulerDelegate.swift ├── Identifier.swift ├── Store.swift ├── ExpoAlarmModule.mm ├── Alarms.swift ├── Manager.swift ├── Alarm.swift ├── NotificationScheduler.swift └── ExpoAlarmModule.swift ├── .gitattributes ├── tsconfig.build.json ├── babel.config.js ├── plugin ├── tsconfig.json └── src │ └── withExpoAlarmModule.ts ├── android ├── gradle.properties ├── src │ └── main │ │ ├── res │ │ └── values │ │ │ └── strings.xml │ │ ├── java │ │ └── com │ │ │ └── expoalarmmodule │ │ │ ├── receivers │ │ │ ├── BootReceiver.java │ │ │ ├── AlarmReceiver.java │ │ │ └── NotificationActionReceiver.java │ │ │ ├── ExpoAlarmModulePackage.java │ │ │ ├── AlarmService.java │ │ │ ├── GsonUtil.java │ │ │ ├── Storage.java │ │ │ ├── AlarmDates.java │ │ │ ├── Alarm.java │ │ │ ├── Sound.java │ │ │ ├── Manager.java │ │ │ ├── ExpoAlarmModuleModule.java │ │ │ └── Helper.java │ │ └── AndroidManifest.xml └── build.gradle ├── lefthook.yml ├── .editorconfig ├── .yarnrc.yml ├── tsconfig.json ├── turbo.json ├── scripts └── bootstrap.js ├── LICENSE ├── .gitignore ├── CONTRIBUTING.md ├── expo-alarm-module.podspec ├── package.json ├── README.md └── CODE_OF_CONDUCT.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /example/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /src/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | it.todo('write a test'); 2 | -------------------------------------------------------------------------------- /app.plugin.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./plugin/build/withExpoAlarmModule'); 2 | -------------------------------------------------------------------------------- /example/ios/.xcode.env.local: -------------------------------------------------------------------------------- 1 | export NODE_BINARY=/opt/homebrew/bin/node 2 | 3 | -------------------------------------------------------------------------------- /example/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'react-native', 3 | }; 4 | -------------------------------------------------------------------------------- /ios/bell.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nidilap/expo-alarm-module/HEAD/ios/bell.mp3 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | # specific for windows script files 3 | *.bat text eol=crlf -------------------------------------------------------------------------------- /example/.bundle/config: -------------------------------------------------------------------------------- 1 | BUNDLE_PATH: "vendor/bundle" 2 | BUNDLE_FORCE_RUBY_PLATFORM: 1 3 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "exclude": ["example"] 4 | } 5 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:@react-native/babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /example/ios/File.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // ExpoAlarmModuleExample 4 | // 5 | 6 | import Foundation 7 | -------------------------------------------------------------------------------- /ios/ExpoAlarmModule-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ExpoAlarmModuleExample", 3 | "displayName": "ExpoAlarmModuleExample" 4 | } 5 | -------------------------------------------------------------------------------- /example/android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nidilap/expo-alarm-module/HEAD/example/android/app/debug.keystore -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ExpoAlarmModuleExample 3 | 4 | -------------------------------------------------------------------------------- /example/ios/ExpoAlarmModuleExample/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ios/AlarmApplicationDelegate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol AlarmApplicationDelegate { 4 | func playSound(_ soundName: String) 5 | } 6 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nidilap/expo-alarm-module/HEAD/example/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /example/ios/ExpoAlarmModuleExample-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | -------------------------------------------------------------------------------- /example/ios/ExpoAlarmModuleExample/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : RCTAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nidilap/expo-alarm-module/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nidilap/expo-alarm-module/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nidilap/expo-alarm-module/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nidilap/expo-alarm-module/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nidilap/expo-alarm-module/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nidilap/expo-alarm-module/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nidilap/expo-alarm-module/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nidilap/expo-alarm-module/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nidilap/expo-alarm-module/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nidilap/expo-alarm-module/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import { AppRegistry } from 'react-native'; 2 | import App from './src/App'; 3 | import { name as appName } from './app.json'; 4 | 5 | AppRegistry.registerComponent(appName, () => App); 6 | -------------------------------------------------------------------------------- /example/react-native.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pak = require('../package.json'); 3 | 4 | module.exports = { 5 | dependencies: { 6 | [pak.name]: { 7 | root: path.join(__dirname, '..'), 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo-module-scripts/tsconfig.plugin", 3 | "compilerOptions": { 4 | "outDir": "build", 5 | "rootDir": "src" 6 | }, 7 | "include": ["./src"], 8 | "exclude": ["**/__mocks__/*", "**/__tests__/*"] 9 | } 10 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | ExpoAlarmModule_kotlinVersion=1.7.0 2 | ExpoAlarmModule_minSdkVersion=21 3 | ExpoAlarmModule_targetSdkVersion=31 4 | ExpoAlarmModule_compileSdkVersion=31 5 | ExpoAlarmModule_ndkversion=21.4.7075529 6 | android.useAndroidX=true 7 | android.enableJetifier=true 8 | -------------------------------------------------------------------------------- /example/ios/ExpoAlarmModuleExample/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | @autoreleasepool { 8 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'ExpoAlarmModuleExample' 2 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 3 | include ':app' 4 | includeBuild('../node_modules/@react-native/gradle-plugin') 5 | -------------------------------------------------------------------------------- /android/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | expo-alarm-module 4 | expo-alarm-channel 5 | Ativa o alarm 6 | 7 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /example/ios/ExpoAlarmModuleExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/ExpoAlarmModuleExample.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | parallel: true 3 | commands: 4 | lint: 5 | glob: "*.{js,ts,jsx,tsx}" 6 | run: npx eslint {staged_files} 7 | types: 8 | glob: "*.{js,ts, jsx, tsx}" 9 | run: npx tsc --noEmit 10 | commit-msg: 11 | parallel: true 12 | commands: 13 | commitlint: 14 | run: npx commitlint --edit 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 2 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | nmHoistingLimits: workspaces 3 | 4 | plugins: 5 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 6 | spec: "@yarnpkg/plugin-interactive-tools" 7 | - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs 8 | spec: "@yarnpkg/plugin-workspace-tools" 9 | 10 | yarnPath: .yarn/releases/yarn-3.6.1.cjs 11 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /example/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version 4 | ruby ">= 2.6.10" 5 | 6 | # Cocoapods 1.15 introduced a bug which break the build. We will remove the upper 7 | # bound in the template on Cocoapods with next React Native release. 8 | gem 'cocoapods', '>= 1.13', '< 1.15' 9 | gem 'activesupport', '>= 6.1.7.5', '< 7.1.0' 10 | -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pak = require('../package.json'); 3 | 4 | module.exports = { 5 | presets: ['module:@react-native/babel-preset'], 6 | plugins: [ 7 | [ 8 | 'module-resolver', 9 | { 10 | extensions: ['.tsx', '.ts', '.js', '.json'], 11 | alias: { 12 | [pak.name]: path.join(__dirname, '..', pak.source), 13 | }, 14 | }, 15 | ], 16 | ], 17 | }; 18 | -------------------------------------------------------------------------------- /example/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /example/ios/.xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | 6 | # NODE_BINARY variable contains the PATH to the node executable. 7 | # 8 | # Customize the NODE_BINARY variable here. 9 | # For example, to use nvm with brew, add the following line 10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 11 | export NODE_BINARY=$(command -v node) 12 | -------------------------------------------------------------------------------- /ios/NotificationSchedulerDelegate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | protocol NotificationSchedulerDelegate { 5 | func requestAuthorization() 6 | func registerNotificationCategories() 7 | func setNotification(alarm: Alarm) 8 | func setNotificationForSnooze(ringtoneName: String, snoozeMinute: Int, uid: String) 9 | func cancelNotification(ByUUIDStr uid: String) 10 | func updateNotification(ByUUIDStr uid: String, date: Date, ringtoneName: String, snoonzeEnabled: Bool) 11 | func syncAlarmStateWithNotification() 12 | } 13 | 14 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | buildToolsVersion = "34.0.0" 4 | minSdkVersion = 21 5 | compileSdkVersion = 34 6 | targetSdkVersion = 34 7 | ndkVersion = "25.1.8937393" 8 | kotlinVersion = "1.8.0" 9 | } 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | dependencies { 15 | classpath("com.android.tools.build:gradle") 16 | classpath("com.facebook.react:react-native-gradle-plugin") 17 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") 18 | } 19 | } 20 | 21 | apply plugin: "com.facebook.react.rootproject" 22 | -------------------------------------------------------------------------------- /android/src/main/java/com/expoalarmmodule/receivers/BootReceiver.java: -------------------------------------------------------------------------------- 1 | package com.expoalarmmodule.receivers; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.util.Log; 7 | 8 | import com.expoalarmmodule.Manager; 9 | 10 | public class BootReceiver extends BroadcastReceiver { 11 | 12 | private static final String TAG = "AlarmBootReceiver"; 13 | 14 | @Override 15 | public void onReceive(Context context, Intent intent) { 16 | String action = intent.getAction(); 17 | if (action != null && action.equals("android.intent.action.BOOT_COMPLETED")) { 18 | Log.d(TAG, "received on boot intent: " + action); 19 | Manager.reschedule(context); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": ".", 4 | "paths": { 5 | "expo-alarm-module": ["./src/index"] 6 | }, 7 | "allowUnreachableCode": false, 8 | "allowUnusedLabels": false, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "jsx": "react", 12 | "lib": ["esnext"], 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitReturns": true, 17 | "noImplicitUseStrict": false, 18 | "noStrictGenericChecks": false, 19 | "noUncheckedIndexedAccess": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "resolveJsonModule": true, 23 | "skipLibCheck": true, 24 | "strict": true, 25 | "target": "esnext", 26 | "verbatimModuleSyntax": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "pipeline": { 4 | "build:android": { 5 | "inputs": [ 6 | "package.json", 7 | "android", 8 | "!android/build", 9 | "src/*.ts", 10 | "src/*.tsx", 11 | "example/package.json", 12 | "example/android", 13 | "!example/android/.gradle", 14 | "!example/android/build", 15 | "!example/android/app/build" 16 | ], 17 | "outputs": [] 18 | }, 19 | "build:ios": { 20 | "inputs": [ 21 | "package.json", 22 | "*.podspec", 23 | "ios", 24 | "src/*.ts", 25 | "src/*.tsx", 26 | "example/package.json", 27 | "example/ios", 28 | "!example/ios/build", 29 | "!example/ios/Pods" 30 | ], 31 | "outputs": [] 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /example/ios/ExpoAlarmModuleExampleTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /scripts/bootstrap.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const path = require('path'); 3 | const child_process = require('child_process'); 4 | 5 | const root = path.resolve(__dirname, '..'); 6 | const args = process.argv.slice(2); 7 | const options = { 8 | cwd: process.cwd(), 9 | env: process.env, 10 | stdio: 'inherit', 11 | encoding: 'utf-8', 12 | }; 13 | 14 | if (os.type() === 'Windows_NT') { 15 | options.shell = true; 16 | } 17 | 18 | let result; 19 | 20 | if (process.cwd() !== root || args.length) { 21 | // We're not in the root of the project, or additional arguments were passed 22 | // In this case, forward the command to `yarn` 23 | result = child_process.spawnSync('yarn', args, options); 24 | } else { 25 | // If `yarn` is run without arguments, perform bootstrap 26 | result = child_process.spawnSync('yarn', ['bootstrap'], options); 27 | } 28 | 29 | process.exitCode = result.status; 30 | -------------------------------------------------------------------------------- /example/ios/ExpoAlarmModuleExample/AppDelegate.mm: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | 5 | @implementation AppDelegate 6 | 7 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 8 | { 9 | self.moduleName = @"ExpoAlarmModuleExample"; 10 | // You can add your custom initial props in the dictionary below. 11 | // They will be passed down to the ViewController used by React Native. 12 | self.initialProps = @{}; 13 | 14 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 15 | } 16 | 17 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 18 | { 19 | return [self getBundleURL]; 20 | } 21 | 22 | - (NSURL *)getBundleURL 23 | { 24 | #if DEBUG 25 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; 26 | #else 27 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 28 | #endif 29 | } 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /android/src/main/java/com/expoalarmmodule/ExpoAlarmModulePackage.java: -------------------------------------------------------------------------------- 1 | package com.expoalarmmodule; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.facebook.react.ReactPackage; 6 | import com.facebook.react.bridge.NativeModule; 7 | import com.facebook.react.bridge.ReactApplicationContext; 8 | import com.facebook.react.uimanager.ViewManager; 9 | 10 | import java.util.ArrayList; 11 | import java.util.Collections; 12 | import java.util.List; 13 | 14 | public class ExpoAlarmModulePackage implements ReactPackage { 15 | @NonNull 16 | @Override 17 | public List createNativeModules(@NonNull ReactApplicationContext reactContext) { 18 | List modules = new ArrayList<>(); 19 | modules.add(new ExpoAlarmModuleModule(reactContext)); 20 | return modules; 21 | } 22 | 23 | @NonNull 24 | @Override 25 | public List createViewManagers(@NonNull ReactApplicationContext reactContext) { 26 | return Collections.emptyList(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ios/Identifier.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Identifier { 4 | static let stopActionIdentifier = "ALARM_IOS_SWIFT_STOP" 5 | static let snoozeActionIdentifier = "ALARM_IOS_SWIFT_SNOOZE" 6 | static let alarmCategoryIndentifier = "ALARM_CATEGORY" 7 | static let snoozeAlarmCategoryIndentifier = "SNOOZE_ALARM_CATEGORY" 8 | 9 | static let addSegueIdentifier = "addSegue" 10 | static let editSegueIdentifier = "editSegue" 11 | static let saveSegueIdentifier = "saveEditSegue" 12 | static let soundSegueIdentifier = "soundSegue" 13 | static let labelSegueIdentifier = "labelEditSegue" 14 | static let weekdaysSegueIdentifier = "weekdaysSegue" 15 | static let settingIdentifier = "setting" 16 | static let musicIdentifier = "musicIdentifier" 17 | static let alarmCellIdentifier = "alarmCell" 18 | 19 | static let labelUnwindIdentifier = "labelUnwindSegue" 20 | static let soundUnwindIdentifier = "soundUnwindSegue" 21 | static let weekdaysUnwindIdentifier = "weekdaysUnwindSegue" 22 | } 23 | -------------------------------------------------------------------------------- /src/utils.tsx: -------------------------------------------------------------------------------- 1 | function getParam(param: any, key: any) { 2 | if (param && (param[key] !== null || param[key] !== undefined)) { 3 | return param[key]; 4 | } 5 | } 6 | 7 | function toAndroidDays(daysArray: string | Date | number[] | undefined) { 8 | if (daysArray) { 9 | if (Array.isArray(daysArray)) { 10 | return daysArray.map((day) => (day + 1) % 7); 11 | } else { 12 | return daysArray; 13 | } 14 | } else { 15 | return []; 16 | } 17 | } 18 | 19 | function fromAndroidDays(daysArray: string | Date | number[] | undefined) { 20 | if (daysArray) { 21 | if (Array.isArray(daysArray)) { 22 | return daysArray.map((d) => (d === 0 ? 6 : d - 1)); 23 | } else { 24 | return daysArray; 25 | } 26 | } else { 27 | return []; 28 | } 29 | } 30 | 31 | function fromIOSDays(dayUTCSeconds: number): Date | undefined { 32 | if (dayUTCSeconds) { 33 | return new Date(new Date(0).setUTCSeconds(dayUTCSeconds)); 34 | } else { 35 | return; 36 | } 37 | } 38 | 39 | export { getParam, toAndroidDays, fromAndroidDays, fromIOSDays }; 40 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /android/src/main/java/com/expoalarmmodule/receivers/AlarmReceiver.java: -------------------------------------------------------------------------------- 1 | package com.expoalarmmodule.receivers; 2 | 3 | 4 | import android.content.BroadcastReceiver; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.os.Build; 8 | import android.util.Log; 9 | import android.widget.Toast; 10 | 11 | import com.expoalarmmodule.AlarmService; 12 | import com.expoalarmmodule.Manager; 13 | 14 | public class AlarmReceiver extends BroadcastReceiver { 15 | 16 | private static final String TAG = "AlarmReceiver"; 17 | 18 | @Override 19 | public void onReceive(Context context, Intent intent) { 20 | String alarmUid = intent.getStringExtra("ALARM_UID"); 21 | 22 | Intent serviceIntent = new Intent(context, AlarmService.class); 23 | serviceIntent.putExtra("ALARM_UID", alarmUid); 24 | serviceIntent.putExtras(serviceIntent); 25 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 26 | context.startForegroundService(serviceIntent); 27 | } else { 28 | context.startService(serviceIntent); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Nidilap 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /example/ios/ExpoAlarmModuleExample/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "scale" : "1x", 46 | "size" : "1024x1024" 47 | } 48 | ], 49 | "info" : { 50 | "author" : "xcode", 51 | "version" : 1 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ios/Store.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class Store { 4 | let alarms: Alarms = load() 5 | // singleton 6 | static let shared = Store() 7 | static let changedNotification = Notification.Name("StoreChanged") 8 | 9 | func save(_ data: Alarms, notifying: Alarm?, userInfo: [AnyHashable: Any]) { 10 | if let jsonData = try? JSONEncoder().encode(data) { 11 | UserDefaults.standard.set(jsonData, forKey: .UserDefaultsKey) 12 | } 13 | NotificationCenter.default.post(name: Store.changedNotification, object: notifying, userInfo: userInfo) 14 | } 15 | 16 | static func load() -> Alarms{ 17 | if let data = UserDefaults.standard.data(forKey: .UserDefaultsKey) { 18 | if let alarms = try? JSONDecoder().decode(Alarms.self, from: data) { 19 | return alarms 20 | } 21 | } 22 | return Alarms() 23 | } 24 | 25 | func clear() { 26 | for key in UserDefaults.standard.dictionaryRepresentation().keys { 27 | UserDefaults.standard.removeObject(forKey: key.description) 28 | } 29 | } 30 | } 31 | 32 | fileprivate extension String { 33 | static let UserDefaultsKey = "UserDefaultsData" 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # XDE 6 | .expo/ 7 | 8 | # VSCode 9 | .vscode/ 10 | jsconfig.json 11 | 12 | # Xcode 13 | # 14 | build/ 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | *.xccheckout 25 | *.moved-aside 26 | DerivedData 27 | *.hmap 28 | *.ipa 29 | *.xcuserstate 30 | project.xcworkspace 31 | 32 | # Android/IJ 33 | # 34 | .classpath 35 | .cxx 36 | .gradle 37 | .idea 38 | .project 39 | .settings 40 | local.properties 41 | android.iml 42 | 43 | # Cocoapods 44 | # 45 | example/ios/Pods 46 | 47 | # Ruby 48 | example/vendor/ 49 | 50 | # node.js 51 | # 52 | node_modules/ 53 | npm-debug.log 54 | yarn-debug.log 55 | yarn-error.log 56 | 57 | # BUCK 58 | buck-out/ 59 | \.buckd/ 60 | android/app/libs 61 | android/keystores/debug.keystore 62 | 63 | # Yarn 64 | .yarn/* 65 | !.yarn/patches 66 | !.yarn/plugins 67 | !.yarn/releases 68 | !.yarn/sdks 69 | !.yarn/versions 70 | 71 | # Expo 72 | .expo/ 73 | .expo/* 74 | 75 | !plugin/build 76 | 77 | # Turborepo 78 | .turbo/ 79 | 80 | # generated by bob 81 | lib/ 82 | example/node_modules/ 83 | 84 | #Android Example 85 | 86 | example/android/ 87 | 88 | #Plugin build 89 | plugin/build -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "expo-alarm-module-example", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "android": "react-native run-android", 7 | "ios": "react-native run-ios", 8 | "start": "react-native start", 9 | "build:android": "cd android && ./gradlew assembleDebug --no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a", 10 | "build:ios": "cd ios && xcodebuild -workspace ExpoAlarmModuleExample.xcworkspace -scheme ExpoAlarmModuleExample -configuration Debug -sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO" 11 | }, 12 | "dependencies": { 13 | "react": "18.2.0", 14 | "react-native": "0.73.6", 15 | "react-native-uuid": "^2.0.1" 16 | }, 17 | "devDependencies": { 18 | "@babel/core": "^7.20.0", 19 | "@babel/preset-env": "^7.20.0", 20 | "@babel/runtime": "^7.20.0", 21 | "@react-native/babel-preset": "0.73.21", 22 | "@react-native/metro-config": "0.73.5", 23 | "@react-native/typescript-config": "0.73.1", 24 | "babel-plugin-module-resolver": "^5.0.0", 25 | "pod-install": "^0.1.0" 26 | }, 27 | "engines": { 28 | "node": ">=18" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); 2 | const path = require('path'); 3 | const escape = require('escape-string-regexp'); 4 | const exclusionList = require('metro-config/src/defaults/exclusionList'); 5 | const pak = require('../package.json'); 6 | 7 | const root = path.resolve(__dirname, '..'); 8 | const modules = Object.keys({ ...pak.peerDependencies }); 9 | 10 | /** 11 | * Metro configuration 12 | * https://facebook.github.io/metro/docs/configuration 13 | * 14 | * @type {import('metro-config').MetroConfig} 15 | */ 16 | const config = { 17 | watchFolders: [root], 18 | 19 | // We need to make sure that only one version is loaded for peerDependencies 20 | // So we block them at the root, and alias them to the versions in example's node_modules 21 | resolver: { 22 | blacklistRE: exclusionList(modules.map((m) => new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`))), 23 | 24 | extraNodeModules: modules.reduce((acc, name) => { 25 | acc[name] = path.join(__dirname, 'node_modules', name); 26 | return acc; 27 | }, {}), 28 | }, 29 | 30 | transformer: { 31 | getTransformOptions: async () => ({ 32 | transform: { 33 | experimentalImportSupport: false, 34 | inlineRequires: true, 35 | }, 36 | }), 37 | }, 38 | }; 39 | 40 | module.exports = mergeConfig(getDefaultConfig(__dirname), config); 41 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | For contributing, install the packages with yarn. 4 | After installing and making the changes, build with "bob build". 5 | For commiting, use git add . && git commit -am "comment" 6 | 7 | After this, commit the changes with the new version and publish using "npm publish". 8 | 9 | 10 | Contributions are always welcome, no matter how large or small! 11 | 12 | We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project. Before contributing, please read the [code of conduct](./CODE_OF_CONDUCT.md). 13 | 14 | ## Development workflow 15 | 16 | To run the example for testing, run the following commands: 17 | ```sh 18 | yarn example start 19 | ``` 20 | 21 | To run the example app on Android: 22 | 23 | ```sh 24 | yarn example android 25 | ``` 26 | 27 | To run the example app on iOS: 28 | 29 | ```sh 30 | yarn example ios 31 | ``` 32 | 33 | After doing changes in the android folder, run .\gradlew clean and .\gradlew build in the example/android. 34 | 35 | 36 | For testing without publishing to npm, do the following: 37 | 38 | - Run npm pack in the package 39 | - Install the package in a managed Expo project ```npx expo install expo-alarm-module@../expo-alarm-module-1.1.10.tgz``` (Here will be the path to the package, in my case it was outside the project). 40 | - Then add "expo-alarm-module" to the plugins array and build the native app locally with expo prebuild and yarn ios, yarn android. -------------------------------------------------------------------------------- /android/src/main/java/com/expoalarmmodule/AlarmService.java: -------------------------------------------------------------------------------- 1 | package com.expoalarmmodule; 2 | 3 | import android.app.Notification; 4 | import android.app.Service; 5 | import android.content.Intent; 6 | import android.os.IBinder; 7 | import android.util.Log; 8 | 9 | public class AlarmService extends Service { 10 | 11 | private static final String TAG = "AlarmService"; 12 | 13 | @Override 14 | public IBinder onBind(final Intent intent) { 15 | Log.d(TAG, "On bind " + intent.getExtras()); 16 | return null; 17 | } 18 | 19 | // called only the first time we start the service 20 | @Override 21 | public void onCreate() { 22 | super.onCreate(); 23 | Log.d(TAG, "Creating service"); 24 | } 25 | @Override 26 | public void onDestroy() { 27 | super.onDestroy(); 28 | Log.d(TAG, "Stopping service"); 29 | } 30 | 31 | // triggered every time we call startService() when we start our service 32 | @Override 33 | public int onStartCommand(final Intent intent, final int flags, final int startId) { 34 | super.onStartCommand(intent, flags, startId); 35 | Log.d(TAG, "On start command"); 36 | 37 | String alarmUid = intent.getStringExtra("ALARM_UID"); 38 | Alarm alarm = Storage.getAlarm(getApplicationContext(), alarmUid); 39 | Notification notification = Helper.getAlarmNotification(this, alarm, 1); 40 | Manager.start(getApplicationContext(), alarmUid); 41 | startForeground(1, notification); 42 | 43 | // service will be explicitly started and stopped 44 | return START_STICKY; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /plugin/src/withExpoAlarmModule.ts: -------------------------------------------------------------------------------- 1 | import { AndroidConfig, type ConfigPlugin, createRunOncePlugin, withInfoPlist } from '@expo/config-plugins'; 2 | 3 | const pkg = require('expo-alarm-module/package.json'); 4 | 5 | const withIosPermissions: ConfigPlugin = (configProp) => { 6 | return withInfoPlist(configProp, (config) => { 7 | const currentBackgroundModes = config.modResults.UIBackgroundModes ?? []; 8 | 9 | // Audio background capability 10 | if (!currentBackgroundModes.includes('audio')) { 11 | config.modResults.UIBackgroundModes = [...currentBackgroundModes, 'audio']; 12 | } 13 | 14 | // Add background capability for "remote-notification" 15 | if (!currentBackgroundModes.includes('remote-notification')) { 16 | config.modResults.UIBackgroundModes = [...currentBackgroundModes, 'remote-notification']; 17 | } 18 | 19 | return config; 20 | }); 21 | }; 22 | 23 | const withAndroidPermissions: ConfigPlugin = (config) => { 24 | return AndroidConfig.Permissions.withPermissions(config, [ 25 | 'android.permission.SCHEDULE_EXACT_ALARM', 26 | 'android.permission.VIBRATE', 27 | 'android.permission.RECEIVE_BOOT_COMPLETED', 28 | 'android.permission.FOREGROUND_SERVICE', 29 | 'android.permission.WAKE_LOCK', 30 | 'android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK', 31 | 'android.permission.POST_NOTIFICATIONS', 32 | ]); 33 | }; 34 | 35 | const withExpoAlarmModule: ConfigPlugin = (config) => { 36 | config = withIosPermissions(config); 37 | config = withAndroidPermissions(config); 38 | 39 | return config; 40 | }; 41 | 42 | export default createRunOncePlugin(withExpoAlarmModule, pkg.name, pkg.version); 43 | -------------------------------------------------------------------------------- /src/types/Alarm.types.ts: -------------------------------------------------------------------------------- 1 | export type AlarmSettings = { 2 | /** 3 | * Id of the Alarm 4 | */ 5 | uid: string; 6 | 7 | /** 8 | * Day the alarm will be triggered. It can be a string, a Date object or an array of numbers. 9 | */ 10 | day: string | Date | number[]; 11 | 12 | /** 13 | * Title of the alarm 14 | */ 15 | title: string; 16 | 17 | /** 18 | * If the alarm is active. Inactive alarms will not be triggered. 19 | */ 20 | active: boolean; 21 | 22 | /** 23 | * Description of the alarm. 24 | */ 25 | description?: string; 26 | 27 | /** 28 | * Hour the alarm will be triggered. Not required if day is a Date object. 29 | */ 30 | hour?: number | undefined; 31 | 32 | /** 33 | * Minutes the alarm will be triggered. Not required if day is a Date object. 34 | */ 35 | minutes?: number | undefined; 36 | 37 | /** 38 | * If the dismiss button should be shown in the notification. It is false by default 39 | */ 40 | showDismiss?: boolean | undefined; 41 | 42 | /** 43 | * Custom dismiss text for the button of the notification. 44 | */ 45 | dismissText?: string | undefined; 46 | 47 | /** 48 | * If the snooze of the alarm is active. False by default 49 | */ 50 | showSnooze?: boolean | undefined; 51 | 52 | /** 53 | * Interval in minutes to snooze the alarm. It is 5 minutes by default. 54 | */ 55 | snoozeInterval?: number | undefined; 56 | 57 | /** 58 | * Custom snooze text for the button of the notification. 59 | */ 60 | snoozeText?: string | undefined; 61 | 62 | /** 63 | * If the alarm needs to repeat. 64 | */ 65 | repeating?: boolean | undefined; 66 | }; 67 | -------------------------------------------------------------------------------- /android/src/main/java/com/expoalarmmodule/GsonUtil.java: -------------------------------------------------------------------------------- 1 | package com.expoalarmmodule; 2 | 3 | import android.os.Build; 4 | 5 | import com.google.gson.Gson; 6 | import com.google.gson.GsonBuilder; 7 | 8 | import java.time.LocalDate; 9 | import java.time.ZoneId; 10 | import java.time.ZonedDateTime; 11 | import java.time.format.DateTimeFormatter; 12 | import io.goodforgod.gson.configuration.deserializer.LocalDateDeserializer; 13 | import io.goodforgod.gson.configuration.deserializer.ZonedDateTimeDeserializer; 14 | import io.goodforgod.gson.configuration.serializer.LocalDateSerializer; 15 | import io.goodforgod.gson.configuration.serializer.ZonedDateTimeSerializer; 16 | 17 | public class GsonUtil { 18 | // Constructor 19 | private GsonUtil() { 20 | } 21 | 22 | // General 23 | public static Gson create() { 24 | return new GsonBuilder().create(); 25 | } 26 | 27 | public static Gson createSerialize() { 28 | // UTC format for date and time 29 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 30 | return new GsonBuilder() 31 | .setDateFormat("yyyy-MM-dd'T'HH:mm:ss") 32 | .registerTypeAdapter(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ISO_LOCAL_DATE)) 33 | .registerTypeAdapter(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ISO_LOCAL_DATE)) 34 | .registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeSerializer(DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneId.of("UTC")))) 35 | .registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeDeserializer(DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneId.of("UTC")))) 36 | .create(); 37 | } else { 38 | return new GsonBuilder().create(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ios/ExpoAlarmModule.mm: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface RCT_EXTERN_MODULE(ExpoAlarmModule, NSObject) 4 | 5 | RCT_EXTERN_METHOD(multiply:(float)a withB:(float)b 6 | withResolver:(RCTPromiseResolveBlock)resolve 7 | withRejecter:(RCTPromiseRejectBlock)reject) 8 | 9 | 10 | RCT_EXTERN_METHOD(set:(NSDictionary)alarm 11 | withResolver:(RCTPromiseResolveBlock)resolve 12 | withRejecter:(RCTPromiseRejectBlock)reject) 13 | 14 | RCT_EXTERN_METHOD(enable:(NSString)alarmUid 15 | withResolver:(RCTPromiseResolveBlock)resolve 16 | withRejecter:(RCTPromiseRejectBlock)reject) 17 | 18 | RCT_EXTERN_METHOD(disable:(NSString)alarmUid 19 | withResolver:(RCTPromiseResolveBlock)resolve 20 | withRejecter:(RCTPromiseRejectBlock)reject) 21 | 22 | RCT_EXTERN_METHOD(stop) 23 | 24 | RCT_EXTERN_METHOD(get:(NSString)alarmUid 25 | withResolver:(RCTPromiseResolveBlock)resolve 26 | withRejecter:(RCTPromiseRejectBlock)reject) 27 | 28 | RCT_EXTERN_METHOD(getAll:(RCTPromiseResolveBlock)resolve 29 | withRejecter:(RCTPromiseRejectBlock)reject) 30 | 31 | RCT_EXTERN_METHOD(remove:(NSString)alarmUid 32 | withResolver:(RCTPromiseResolveBlock)resolve 33 | withRejecter:(RCTPromiseRejectBlock)reject) 34 | 35 | RCT_EXTERN_METHOD(removeAll:(RCTPromiseResolveBlock)resolve 36 | withRejecter:(RCTPromiseRejectBlock)reject) 37 | 38 | RCT_EXTERN_METHOD(getState:(RCTPromiseResolveBlock)resolve 39 | withRejecter:(RCTPromiseRejectBlock)reject) 40 | 41 | 42 | + (BOOL)requiresMainQueueSetup 43 | { 44 | return NO; 45 | } 46 | 47 | 48 | 49 | 50 | @end 51 | -------------------------------------------------------------------------------- /expo-alarm-module.podspec: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | package = JSON.parse(File.read(File.join(__dir__, "package.json"))) 4 | folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' 5 | 6 | Pod::Spec.new do |s| 7 | s.name = "expo-alarm-module" 8 | s.version = package["version"] 9 | s.summary = package["description"] 10 | s.homepage = package["homepage"] 11 | s.license = package["license"] 12 | s.authors = package["author"] 13 | 14 | s.platforms = { :ios => "11.0" } 15 | s.source = { :git => "https://github.com/Nidilap/expo-alarm-module.git", :tag => "#{s.version}" } 16 | 17 | s.source_files = "ios/**/*.{h,m,mm,swift}" 18 | s.resource = "ios/bell.mp3" 19 | 20 | # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. 21 | # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79. 22 | if respond_to?(:install_modules_dependencies, true) 23 | install_modules_dependencies(s) 24 | else 25 | s.dependency "React-Core" 26 | 27 | # Don't install the dependencies when we run `pod install` in the old architecture. 28 | if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then 29 | s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1" 30 | s.pod_target_xcconfig = { 31 | "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"", 32 | "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1", 33 | "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" 34 | } 35 | s.dependency "React-Codegen" 36 | s.dependency "RCT-Folly" 37 | s.dependency "RCTRequired" 38 | s.dependency "RCTTypeSafety" 39 | s.dependency "ReactCommon/turbomodule/core" 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /example/ios/ExpoAlarmModuleExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ExpoAlarmModuleExample 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(CURRENT_PROJECT_VERSION) 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSAllowsArbitraryLoads 30 | 31 | NSAllowsLocalNetworking 32 | 33 | 34 | NSLocationWhenInUseUsageDescription 35 | 36 | UIBackgroundModes 37 | 38 | remote-notification 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UIViewControllerBasedStatusBarAppearance 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m 13 | org.gradle.jvmargs=-Xmx4608m -XX:MaxMetaspaceSize=512m 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | # Automatically convert third-party libraries to use AndroidX 25 | android.enableJetifier=true 26 | 27 | # Use this property to specify which architecture you want to build. 28 | # You can also override it from the CLI using 29 | # ./gradlew -PreactNativeArchitectures=x86_64 30 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 31 | 32 | # Use this property to enable support to the new architecture. 33 | # This will allow you to use TurboModules and the Fabric render in 34 | # your application. You should enable this flag either if you want 35 | # to write custom TurboModules/Fabric components OR use libraries that 36 | # are providing them. 37 | newArchEnabled=false 38 | 39 | # Use this property to enable or disable the Hermes JS engine. 40 | # If set to false, you will be using JSC instead. 41 | hermesEnabled=true 42 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Resolve react_native_pods.rb with node to allow for hoisting 2 | require Pod::Executable.execute_command('node', ['-p', 3 | 'require.resolve( 4 | "react-native/scripts/react_native_pods.rb", 5 | {paths: [process.argv[1]]}, 6 | )', __dir__]).strip 7 | 8 | platform :ios, min_ios_version_supported 9 | prepare_react_native_project! 10 | 11 | # If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set. 12 | # because `react-native-flipper` depends on (FlipperKit,...) that will be excluded 13 | # 14 | # To fix this you can also exclude `react-native-flipper` using a `react-native.config.js` 15 | # ```js 16 | # module.exports = { 17 | # dependencies: { 18 | # ...(process.env.NO_FLIPPER ? { 'react-native-flipper': { platforms: { ios: null } } } : {}), 19 | # ``` 20 | flipper_config = ENV['NO_FLIPPER'] == "1" ? FlipperConfiguration.disabled : FlipperConfiguration.enabled 21 | 22 | linkage = ENV['USE_FRAMEWORKS'] 23 | if linkage != nil 24 | Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green 25 | use_frameworks! :linkage => linkage.to_sym 26 | end 27 | 28 | target 'ExpoAlarmModuleExample' do 29 | config = use_native_modules! 30 | 31 | use_react_native!( 32 | :path => config[:reactNativePath], 33 | # Enables Flipper. 34 | # 35 | # Note that if you have use_frameworks! enabled, Flipper will not work and 36 | # you should disable the next line. 37 | :flipper_configuration => flipper_config, 38 | # An absolute path to your application root. 39 | :app_path => "#{Pod::Config.instance.installation_root}/.." 40 | ) 41 | 42 | target 'ExpoAlarmModuleExampleTests' do 43 | inherit! :complete 44 | # Pods for testing 45 | end 46 | 47 | post_install do |installer| 48 | # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 49 | react_native_post_install( 50 | installer, 51 | config[:reactNativePath], 52 | :mac_catalyst_enabled => false 53 | ) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /src/models/Alarm.tsx: -------------------------------------------------------------------------------- 1 | import { toAndroidDays, fromAndroidDays, getParam, fromIOSDays } from '../utils'; 2 | 3 | class Alarm { 4 | uid?: string | undefined; 5 | title?: string | undefined; 6 | description?: string | undefined; 7 | hour?: number | undefined; 8 | minutes?: number | undefined; 9 | showDismiss?: boolean | undefined; 10 | dismissText?: string | undefined; 11 | showSnooze?: boolean | undefined; 12 | snoozeInterval?: number | undefined; 13 | snoozeText?: string | undefined; 14 | repeating?: boolean | undefined; 15 | active?: boolean | undefined; 16 | day?: string | Date | number[] | undefined; 17 | 18 | constructor(params: any = null) { 19 | this.uid = getParam(params, 'uid'); 20 | this.title = getParam(params, 'title'); 21 | this.description = getParam(params, 'description'); 22 | this.hour = getParam(params, 'hour'); 23 | this.minutes = getParam(params, 'minutes'); 24 | this.showDismiss = getParam(params, 'showDismiss'); 25 | this.dismissText = getParam(params, 'dismissText'); 26 | this.showSnooze = getParam(params, 'showSnooze'); 27 | this.snoozeInterval = getParam(params, 'snoozeInterval'); 28 | this.snoozeText = getParam(params, 'snoozeText'); 29 | this.repeating = getParam(params, 'repeating'); 30 | this.active = getParam(params, 'active'); 31 | this.day = getParam(params, 'day'); 32 | } 33 | 34 | static getEmpty() { 35 | return new Alarm({ 36 | title: '', 37 | description: '', 38 | hour: 0, 39 | minutes: 0, 40 | repeating: false, 41 | day: [], 42 | }); 43 | } 44 | 45 | toAndroid() { 46 | return { 47 | ...this, 48 | day: toAndroidDays(this.day), 49 | }; 50 | } 51 | 52 | static fromAndroid(alarm: Alarm) { 53 | alarm.day = fromAndroidDays(alarm.day); 54 | return new Alarm(alarm); 55 | } 56 | 57 | static fromIos(alarm: Alarm) { 58 | if (typeof alarm.day === 'number') { 59 | alarm.day = fromIOSDays(alarm.day); 60 | } 61 | return new Alarm(alarm); 62 | } 63 | } 64 | 65 | export default Alarm; 66 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 21 | 25 | 26 | 27 | 28 | 29 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/rn_edit_text_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 21 | 22 | 23 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /android/src/main/java/com/expoalarmmodule/receivers/NotificationActionReceiver.java: -------------------------------------------------------------------------------- 1 | package com.expoalarmmodule.receivers; 2 | 3 | import android.app.NotificationManager; 4 | import android.content.BroadcastReceiver; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.util.Log; 8 | 9 | import com.expoalarmmodule.Manager; 10 | import com.expoalarmmodule.Helper; 11 | import com.expoalarmmodule.AlarmService; 12 | 13 | public class NotificationActionReceiver extends BroadcastReceiver { 14 | 15 | private static final String TAG = "AlarmNotificationActionReceiver"; 16 | 17 | @Override 18 | public void onReceive(Context context, Intent intent) { 19 | 20 | if (intent == null || intent.getAction() == null) { 21 | Log.e(TAG, "Intent or Action is null"); 22 | return; 23 | } 24 | 25 | String action = intent.getAction(); 26 | String alarmUid = intent.getStringExtra("ALARM_UID"); 27 | int notificationId = intent.getIntExtra("NOTIFICATION_ID", -1); 28 | 29 | if (alarmUid == null) { 30 | Log.e(TAG, "Alarm UID is missing!"); 31 | return; 32 | } 33 | 34 | switch (action) { 35 | case "DISMISS_ACTION": 36 | Log.d(TAG, "Received DISMISS action for Alarm: " + alarmUid); 37 | Manager.stop(context); 38 | this.removeNotification(context, notificationId); 39 | break; 40 | 41 | case "SNOOZE_ACTION": 42 | Log.d(TAG, "Received SNOOZE action for Alarm: " + alarmUid); 43 | Manager.snooze(context); 44 | this.removeNotification(context, notificationId); 45 | break; 46 | 47 | default: 48 | Log.e(TAG, "Unknown action received: " + action); 49 | break; 50 | } 51 | } 52 | 53 | private void removeNotification(Context context, int notificationId) { 54 | Intent serviceIntentSnooze = new Intent(context, AlarmService.class); 55 | context.stopService(serviceIntentSnooze); 56 | if (notificationId != -1) { 57 | ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)) 58 | .cancel(notificationId); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /example/ios/ExpoAlarmModuleExampleTests/ExpoAlarmModuleExampleTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import 5 | #import 6 | 7 | #define TIMEOUT_SECONDS 600 8 | #define TEXT_TO_LOOK_FOR @"Welcome to React" 9 | 10 | @interface ExpoAlarmModuleExampleTests : XCTestCase 11 | 12 | @end 13 | 14 | @implementation ExpoAlarmModuleExampleTests 15 | 16 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL (^)(UIView *view))test 17 | { 18 | if (test(view)) { 19 | return YES; 20 | } 21 | for (UIView *subview in [view subviews]) { 22 | if ([self findSubviewInView:subview matching:test]) { 23 | return YES; 24 | } 25 | } 26 | return NO; 27 | } 28 | 29 | - (void)testRendersWelcomeScreen 30 | { 31 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; 32 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 33 | BOOL foundElement = NO; 34 | 35 | __block NSString *redboxError = nil; 36 | #ifdef DEBUG 37 | RCTSetLogFunction( 38 | ^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 39 | if (level >= RCTLogLevelError) { 40 | redboxError = message; 41 | } 42 | }); 43 | #endif 44 | 45 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 46 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 47 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 48 | 49 | foundElement = [self findSubviewInView:vc.view 50 | matching:^BOOL(UIView *view) { 51 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 52 | return YES; 53 | } 54 | return NO; 55 | }]; 56 | } 57 | 58 | #ifdef DEBUG 59 | RCTSetLogFunction(RCTDefaultLogFunction); 60 | #endif 61 | 62 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 63 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 64 | } 65 | 66 | @end 67 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | 7 | dependencies { 8 | classpath "com.android.tools.build:gradle:7.2.1" 9 | } 10 | } 11 | 12 | def isNewArchitectureEnabled() { 13 | return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true" 14 | } 15 | 16 | apply plugin: "com.android.library" 17 | 18 | if (isNewArchitectureEnabled()) { 19 | apply plugin: "com.facebook.react" 20 | } 21 | 22 | def getExtOrDefault(name) { 23 | return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["ExpoAlarmModule_" + name] 24 | } 25 | 26 | def getExtOrIntegerDefault(name) { 27 | return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ExpoAlarmModule_" + name]).toInteger() 28 | } 29 | 30 | def supportsNamespace() { 31 | def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.') 32 | def major = parsed[0].toInteger() 33 | def minor = parsed[1].toInteger() 34 | 35 | // Namespace support was added in 7.3.0 36 | return (major == 7 && minor >= 3) || major >= 8 37 | } 38 | 39 | android { 40 | if (supportsNamespace()) { 41 | namespace "com.expoalarmmodule" 42 | 43 | sourceSets { 44 | main { 45 | manifest.srcFile "src/main/AndroidManifest.xml" 46 | } 47 | } 48 | } 49 | 50 | compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") 51 | 52 | defaultConfig { 53 | minSdkVersion getExtOrIntegerDefault("minSdkVersion") 54 | targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") 55 | 56 | } 57 | 58 | buildTypes { 59 | release { 60 | minifyEnabled false 61 | } 62 | } 63 | 64 | lintOptions { 65 | disable "GradleCompatible" 66 | } 67 | 68 | compileOptions { 69 | sourceCompatibility JavaVersion.VERSION_1_8 70 | targetCompatibility JavaVersion.VERSION_1_8 71 | } 72 | } 73 | 74 | repositories { 75 | mavenCentral() 76 | google() 77 | } 78 | 79 | 80 | dependencies { 81 | // For < 0.71, this will be from the local maven repo 82 | // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin 83 | //noinspection GradleDynamicVersion 84 | implementation "com.facebook.react:react-native:+" 85 | implementation 'com.google.code.gson:gson:2.8.5' 86 | implementation "io.goodforgod:gson-configuration:2.0.0" 87 | } 88 | 89 | -------------------------------------------------------------------------------- /ios/Alarms.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class Alarms: Codable { 4 | private var alarms: [Alarm] 5 | 6 | enum CodingKeys: CodingKey { 7 | case alarms 8 | } 9 | 10 | init() { 11 | self.alarms = [Alarm]() 12 | } 13 | 14 | required init(from decoder: Decoder) throws { 15 | let container: KeyedDecodingContainer = try decoder.container(keyedBy: Alarms.CodingKeys.self) 16 | 17 | self.alarms = try container.decode([Alarm].self, forKey: Alarms.CodingKeys.alarms) 18 | 19 | } 20 | 21 | func getAlarm(ByUUIDStr uuidString: String) -> Alarm?{ 22 | return alarms.first(where: {$0.uid == uuidString}) 23 | } 24 | 25 | func encode(to encoder: Encoder) throws { 26 | var container: KeyedEncodingContainer = encoder.container(keyedBy: Alarms.CodingKeys.self) 27 | 28 | try container.encode(self.alarms, forKey: Alarms.CodingKeys.alarms) 29 | } 30 | 31 | 32 | func add(_ alarm: Alarm) { 33 | alarms.append(alarm) 34 | let newIndex = alarms.firstIndex { $0.uid == alarm.uid }! 35 | Store.shared.save(self, notifying: alarm, userInfo: [ 36 | Alarm.changeReasonKey: Alarm.added, 37 | Alarm.newValueKey: newIndex 38 | ]) 39 | } 40 | 41 | func remove(_ uid: String) { 42 | guard let index = alarms.firstIndex(where: { $0.uid == uid }) else { return } 43 | 44 | let alarm = alarms[index] 45 | let uuidStr = alarm.uid 46 | alarms.remove(at: index) 47 | Store.shared.save(self, notifying: nil, userInfo: [ 48 | Alarm.changeReasonKey: Alarm.removed, 49 | Alarm.oldValueKey: index, 50 | Alarm.newValueKey: uuidStr 51 | ]) 52 | } 53 | 54 | func update(_ alarm: Alarm) { 55 | guard let index = alarms.firstIndex(where: { $0.uid == alarm.uid }) else { return } 56 | Store.shared.save(self, notifying: alarm, userInfo: [ 57 | Alarm.changeReasonKey: Alarm.updated, 58 | Alarm.oldValueKey: index, 59 | Alarm.newValueKey: index 60 | ]) 61 | } 62 | 63 | func getAlarms() -> [Alarm] { 64 | return alarms; 65 | } 66 | 67 | var count: Int { 68 | return alarms.count 69 | } 70 | 71 | var uids: Set { 72 | return Set(alarms.map { $0.uid }) 73 | } 74 | 75 | subscript(index: Int) -> Alarm { 76 | return alarms[index] 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /ios/Manager.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AVFoundation 3 | 4 | class Manager { 5 | private let scheduler: NotificationSchedulerDelegate = NotificationScheduler() 6 | private let alarms: Alarms = Store.shared.alarms 7 | private var currentPlayingAlarm: String? = nil 8 | 9 | func schedule(_ alarm: Alarm) { 10 | // Needs to save beforing set the notification, since the notification depends on the alarm. 11 | 12 | // Stores the alarm in the Store for rescheduling or removing later. 13 | alarms.add(alarm) 14 | 15 | // Creates the notification (the alarm). 16 | scheduler.setNotification(alarm: alarm) 17 | } 18 | 19 | // Gets alarm from store 20 | func getAlarm(_ uid: String) -> Alarm? { 21 | return alarms.getAlarm(ByUUIDStr: uid); 22 | } 23 | 24 | func getAllAlarms() -> [Alarm] { 25 | return alarms.getAlarms(); 26 | } 27 | 28 | 29 | func enable(_ uid: String) { 30 | let alarm: Alarm! = self.getAlarm(uid) ?? nil; 31 | 32 | // Only enables if the alarm already exists and is disabled. 33 | if((alarm != nil) && !alarm.active) { 34 | // Creates the notification (the alarm). 35 | scheduler.setNotification(alarm: alarm) 36 | 37 | alarm.active = true 38 | 39 | alarms.update(alarm) 40 | } 41 | } 42 | 43 | func disable(_ uid: String) { 44 | // Stops any sound of alarm that is playing. 45 | self.stop() 46 | 47 | let alarm: Alarm! = self.getAlarm(uid) ?? nil 48 | 49 | // Only disables if the alarm already exists and is enabled. 50 | if((alarm != nil) && alarm.active) { 51 | // Cancels the notification. 52 | scheduler.cancelNotification(ByUUIDStr: uid) 53 | 54 | // Disables the alarm and save in the Store 55 | alarm.active = false 56 | alarms.update(alarm) 57 | } 58 | } 59 | 60 | func stop() { 61 | setCurrentPlayingAlarm(nil) 62 | ExpoAlarmModule.audioPlayer?.stop() 63 | AudioServicesRemoveSystemSoundCompletion(kSystemSoundID_Vibrate) 64 | } 65 | 66 | func remove(_ uid: String) { 67 | // Stops any sound of alarm that is playing. 68 | self.stop() 69 | 70 | // Cancels the notification 71 | scheduler.cancelNotification(ByUUIDStr: uid) 72 | 73 | // Removes the alarm from the Store. 74 | alarms.remove(uid) 75 | } 76 | 77 | func setCurrentPlayingAlarm(_ uid: String?) { 78 | currentPlayingAlarm = uid; 79 | } 80 | 81 | func getCurrentPlayingAlarm() -> String? { 82 | return currentPlayingAlarm; 83 | } 84 | 85 | func removeAll() { 86 | for alarmUuid in alarms.uids { 87 | self.remove(alarmUuid) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { StyleSheet, View, Button, Text } from 'react-native'; 4 | import { scheduleAlarm, stopAlarm, removeAlarm, enableAlarm, disableAlarm, getAlarm, getAllAlarms, removeAllAlarms, getAlarmState } from 'expo-alarm-module'; 5 | import uuid from 'react-native-uuid'; 6 | 7 | export default function App() { 8 | const [lastAlarmCreated, setLastAlarmCreated] = React.useState(); 9 | 10 | const createAlarm = () => { 11 | var newDate = new Date(); 12 | newDate.setSeconds(newDate.getSeconds() + 60); 13 | 14 | let uuidToUse: string = uuid.v4() as string; 15 | setLastAlarmCreated(uuidToUse); 16 | 17 | scheduleAlarm({ 18 | uid: uuidToUse, 19 | day: newDate, 20 | title: 'Title of alarm', 21 | description: 'Alarm Description', 22 | showDismiss: true, 23 | showSnooze: true, 24 | snoozeText: 'Custom Snooze', 25 | snoozeInterval: 1, 26 | repeating: true, 27 | active: true, 28 | }); 29 | }; 30 | 31 | const removeAlarmCheck = () => { 32 | if (lastAlarmCreated) { 33 | removeAlarm(lastAlarmCreated); 34 | } 35 | }; 36 | 37 | const enableAlarmCheck = () => { 38 | if (lastAlarmCreated) { 39 | enableAlarm(lastAlarmCreated); 40 | } 41 | }; 42 | 43 | const disableAlarmCheck = () => { 44 | if (lastAlarmCreated) { 45 | disableAlarm(lastAlarmCreated); 46 | } 47 | }; 48 | 49 | const getAlarmCheck = async () => { 50 | if (lastAlarmCreated) { 51 | let alarm = await getAlarm(lastAlarmCreated); 52 | console.log('Alarm got: ', alarm); 53 | } 54 | }; 55 | 56 | const getAllAlarmsCheck = async () => { 57 | let resultado = await getAllAlarms(); 58 | console.log(resultado); 59 | }; 60 | 61 | const removeAllAlarmsCheck = async () => { 62 | removeAllAlarms(); 63 | }; 64 | 65 | const getAlarmStateCheck = async () => { 66 | let alarmState = await getAlarmState(); 67 | console.log('Alarm State: ', alarmState); 68 | }; 69 | 70 | return ( 71 | 72 |