Skip to content
This repository has been archived by the owner on Sep 14, 2024. It is now read-only.

Commit

Permalink
feat: v2
Browse files Browse the repository at this point in the history
  • Loading branch information
gtokman committed Feb 10, 2024
1 parent 89eab79 commit 2500f53
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 34 deletions.
70 changes: 67 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,73 @@
# @candlefinance/push
<br/>
<div align="center">
<a alt="npm" href="https://www.npmjs.com/package/@candlefinace/push">
<img alt="npm downloads" src="https://img.shields.io/npm/dm/%40candlefinance%2@candlefinance/push"/>
</a>
<a alt="discord users online" href="https://discord.gg/qnAgjxhg6n"
target="_blank"
rel="noopener noreferrer">
<img alt="discord users online" src="https://img.shields.io/discord/986610142768406548?label=Discord&logo=discord&logoColor=white&cacheSeconds=3600"/>
</div>

Gary Tokman
<br/>

<img src="https://github.com/candlefinance/haptics/assets/12258850/86470cfc-fe84-4159-adcd-dbb659778619.png" alt="Candle Finance" width="200" align="center"/>

<h1 align="center">
Push for iOS
</h1>

<br/>

## Installation

```sh
npm install @candlefinance/push
yarn add @candlefinance/push
```

## Usage

There is no native code required, that is all abstracted away. Here is a simple example of how to use the package:

```js
import push from '@candlefinance/push';

// Shows dialog to request permission to send push notifications, gets APNS token
const isGranted = await push.requestPermissions();

// Get the APNS token w/o showing permission, useful if you want silent push notifications
await registerForToken();

// Check permission status: 'granted', 'denied', or 'notDetermined'
const status = await getAuthorizationStatus();

// Listen for push APNS token
push.addListener('notificationReceived', (data) => {
console.log('notificationReceived', data);
const uuid = data.uuid;
const kind = data.kind; // foreground, background, or opened
const payload = data.payload;
if (uuid) {
// Required push notification to tell iOS that the push was received, if not called, we will call this in 30 seconds
await push.onFinish(uuid);
}
});

push.addListener('deviceTokenReceived', (token) => {
const token: string = token;
});

push.addListener('errorReceived', (data) => {
console.log('errorReceived', data);
});

return () => {
push.removeListener('notificationReceived');
push.removeListener('deviceTokenReceived');
push.removeListener('errorReceived');
};
```

## Contributing

We are open to contributions. Please read our [Contributing Guide](CONTRIBUTING.md) for more information.
14 changes: 4 additions & 10 deletions example/ios/PushExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@
"-ObjC",
"-lc++",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_BUNDLE_IDENTIFIER = com.pushexample.app;
PRODUCT_NAME = PushExample;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
Expand All @@ -504,7 +504,7 @@
"-ObjC",
"-lc++",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_BUNDLE_IDENTIFIER = com.pushexample.app;
PRODUCT_NAME = PushExample;
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
Expand Down Expand Up @@ -580,10 +580,7 @@
"-DFOLLY_USE_LIBCPP=1",
"-DFOLLY_CFG_NO_COROUTINES=1",
);
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;
Expand Down Expand Up @@ -651,10 +648,7 @@
"-DFOLLY_USE_LIBCPP=1",
"-DFOLLY_CFG_NO_COROUTINES=1",
);
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;
Expand Down
1 change: 0 additions & 1 deletion example/ios/PushExample/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<!-- Do not change NSAllowsArbitraryLoads to true, or you will risk app rejection! -->
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSAllowsLocalNetworking</key>
Expand Down
1 change: 1 addition & 0 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"ios": "react-native run-ios",
"start": "react-native start",
"build:android": "cd android && ./gradlew assembleDebug --no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a",
"push": "xcrun simctl push booted com.pushexample.app payload.json",
"build:ios": "cd ios && xcodebuild -workspace PushExample.xcworkspace -scheme PushExample -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"
},
"dependencies": {
Expand Down
9 changes: 9 additions & 0 deletions example/payload.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"aps": {
"alert": {
"title": "Transfer Update",
"body": "Your Transfer has been downloaded"
},
"content-available": 1
}
}
55 changes: 50 additions & 5 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,63 @@
import * as React from 'react';

import { StyleSheet, View, Text } from 'react-native';
import { multiply } from '@candlefinance/push';
import { StyleSheet, View, Text, Button } from 'react-native';
import push, { type AuthorizationStatus } from '@candlefinance/push';

export default function App() {
const [result, setResult] = React.useState<number | undefined>();
const [result, setResult] = React.useState<boolean>(false);
const [status, setStatus] =
React.useState<AuthorizationStatus>('notDetermined');

React.useEffect(() => {
multiply(3, 7).then(setResult);
push.addListener('notificationReceived', (data) => {
console.log('notificationReceived', data);
});

push.addListener('deviceTokenReceived', (data) => {
console.log('deviceTokenReceived', data);
});

push.addListener('errorReceived', (data) => {
console.log('errorReceived', data);
});

return () => {
push.removeListener('notificationReceived');
push.removeListener('deviceTokenReceived');
push.removeListener('errorReceived');
};
}, []);

return (
<View style={styles.container}>
<Text>Result: {result}</Text>
<Text>Is Remote Enabled: {`${result}`}</Text>
<Text>Authorization Status: {status}</Text>
<Button
title="is Registered for Remote Notifications"
onPress={() => {
push.isRegisteredForRemoteNotifications().then((res) => {
console.log('isRegisteredForRemoteNotifications', res);
setResult(res);
});
}}
/>
<Button
title="getAuthorizationStatus"
onPress={() => {
push.getAuthorizationStatus().then((res) => {
console.log('isRegisteredForRemoteNotifications', res);
setStatus(res);
});
}}
/>
<Button
title="Request Permissions"
onPress={() => push.requestPermissions()}
/>
<Button
title="Register for Token"
onPress={() => push.registerForToken()}
/>
</View>
);
}
Expand Down
13 changes: 10 additions & 3 deletions ios/Push.mm
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@ @interface RCT_EXTERN_MODULE(Push, RCTEventEmitter)

RCT_EXTERN_METHOD(supportedEvents)

RCT_EXTERN_METHOD(requestPermissions: (RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(requestPermissions:(RCTPromiseResolveBlock)resolve
withRejecter:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(onFinish:(NSString *)uuid withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(isRegisteredForRemoteNotifications: (RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(isRegisteredForRemoteNotifications:(RCTPromiseResolveBlock)resolve
withRejecter:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(registerForToken:(RCTPromiseResolveBlock)resolve
withRejecter:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(registerForToken: (RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(getAuthorizationStatus:(RCTPromiseResolveBlock)resolve
withRejecter:(RCTPromiseRejectBlock)reject)

+ (BOOL)requiresMainQueueSetup
{
Expand Down
21 changes: 15 additions & 6 deletions ios/Push.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public class Push: RCTEventEmitter {
}

@objc(requestPermissions:withRejecter:)
public func requestPermissions(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
func requestPermissions(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
DispatchQueue.main.async {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
if let error = error {
Expand All @@ -75,16 +75,25 @@ public class Push: RCTEventEmitter {
}
}

@objc(getAuthorizationStatus:withRejecter:)
func getAuthorizationStatus(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
DispatchQueue.main.async {
UNUserNotificationCenter.current().getNotificationSettings { settings in
resolve(settings.authorizationStatus.rawValue)
}
}
}

@objc(registerForToken:withRejecter:)
public func registerForToken(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
func registerForToken(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
resolve(true)
}
}

@objc(isRegisteredForRemoteNotifications:withRejecter:)
public func isRegisteredForRemoteNotifications(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
func isRegisteredForRemoteNotifications(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
DispatchQueue.main.async {
let value = UIApplication.shared.isRegisteredForRemoteNotifications
resolve(value)
Expand Down Expand Up @@ -114,7 +123,7 @@ extension Push: UNUserNotificationCenterDelegate {
let uuid = UUID().uuidString
dispatch(
type: NotificationType.notificationReceived.rawValue,
payload: ["payload": notification.request.content.userInfo, "uuid": uuid]
payload: ["payload": notification.request.content.userInfo, "uuid": uuid, "kind": "foreground"]
)
notificationCallbackDictionary[uuid] = {
completionHandler([.badge, .sound])
Expand All @@ -130,15 +139,15 @@ extension Push: UNUserNotificationCenterDelegate {
public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
dispatch(
type: NotificationType.notificationReceived.rawValue,
payload: ["payload": response.notification.request.content.userInfo]
payload: ["payload": response.notification.request.content.userInfo, "kind": "opened"]
)
}

func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
let uuid = UUID().uuidString
dispatch(
type: NotificationType.notificationReceived.rawValue,
payload: ["payload": userInfo, "uuid": uuid]
payload: ["payload": userInfo, "uuid": uuid, "kind": "background"]
)
notificationCallbackDictionary[uuid] = {
completionHandler(.newData)
Expand Down
41 changes: 35 additions & 6 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ export type NotificationType =
| 'deviceTokenReceived'
| 'errorReceived';

export type AuthorizationStatus =
| 'authorized'
| 'denied'
| 'ephemeral'
| 'notDetermined'
| 'provisional';

class Push {
module: any;
private bridge?: NativeEventEmitter;
Expand Down Expand Up @@ -39,6 +46,34 @@ class Push {
return false;
}

public async onFinish(uuid: string): Promise<void> {
if (Platform.OS === 'ios') {
await this.module.onFinish(uuid);
}
}

// https://developer.apple.com/documentation/usernotifications/unauthorizationstatus?ref=createwithswift.com
public async getAuthorizationStatus(): Promise<AuthorizationStatus> {
if (Platform.OS === 'ios') {
const value: number = await this.module.getAuthorizationStatus();
switch (value) {
case 0:
return 'notDetermined';
case 1:
return 'denied';
case 2:
return 'authorized';
case 3:
return 'provisional';
case 4:
return 'ephemeral';
default:
return 'notDetermined';
}
}
return 'denied';
}

public addListener(
event: NotificationType,
callback: (data: any) => void
Expand All @@ -49,12 +84,6 @@ class Push {
public removeListener(event: NotificationType): void {
this.bridge?.removeAllListeners(event);
}

public async onFinish(uuid: string): Promise<void> {
if (Platform.OS === 'ios') {
await this.module.onFinish(uuid);
}
}
}

const push = new Push();
Expand Down

0 comments on commit 2500f53

Please sign in to comment.