Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions src/Utopia/Messaging/Adapter/Push/OneSignal.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php

namespace Utopia\Messaging\Adapter\Push;

use Utopia\Messaging\Adapter\Push as PushAdapter;
use Utopia\Messaging\Messages\Push as PushMessage;
use Utopia\Messaging\Priority;
use Utopia\Messaging\Response;

class OneSignal extends PushAdapter
{
protected const NAME = 'OneSignal';

/**
* @param string $appId OneSignal App ID
* @param string $apiKey OneSignal REST API Key
*/
public function __construct(
private string $appId,
private string $apiKey,
) {
parent::__construct();
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

/**
* Get adapter name.
*/
public function getName(): string
{
return static::NAME;
}

/**
* Get max messages per request.
*/
public function getMaxMessagesPerRequest(): int
{
return 2000;
}

/**
* {@inheritdoc}
*/
protected function process(PushMessage $message): array
{
$payload = [
'app_id' => $this->appId,
'include_player_ids' => $message->getTo(),
];

if (!\is_null($message->getTitle())) {
$payload['headings'] = ['en' => $message->getTitle()];
}
if (!\is_null($message->getBody())) {
$payload['contents'] = ['en' => $message->getBody()];
}
if (!\is_null($message->getData())) {
$payload['data'] = $message->getData();
}
Comment on lines +57 to +59

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Preserve silent pushes

The common Push message supports contentAvailable, and the shared push tests send data-only messages with contentAvailable: true. OneSignal requires content_available for background/data notifications, but this adapter only sends data, so callers using the silent push contract through OneSignal can get a normal or rejected notification instead of a background notification.

Suggested change
if (!\is_null($message->getData())) {
$payload['data'] = $message->getData();
}
if (!\is_null($message->getData())) {
$payload['data'] = $message->getData();
}
if (!\is_null($message->getContentAvailable())) {
$payload['content_available'] = $message->getContentAvailable();
}

if (!\is_null($message->getAction())) {
$payload['url'] = $message->getAction();
}
if (!\is_null($message->getImage())) {
$payload['big_picture'] = $message->getImage();
$payload['ios_attachments'] = ['id' => $message->getImage()];
}
if (!\is_null($message->getIcon())) {
$payload['small_icon'] = $message->getIcon();
}
if (!\is_null($message->getColor())) {
$payload['android_accent_color'] = $message->getColor();
}
if (!\is_null($message->getTag())) {
$payload['android_group'] = $message->getTag();
}
if (!\is_null($message->getSound())) {
$payload['android_sound'] = $message->getSound();
$payload['ios_sound'] = $message->getSound() . '.wav';
}
Comment on lines +76 to +79

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Do not rewrite sounds

Other push adapters pass the message sound through unchanged, and callers may pass values like default or an already-qualified filename. Appending .wav here changes default to default.wav and chime.wav to chime.wav.wav, which can make iOS use the wrong sound or no custom sound at all.

Suggested change
if (!\is_null($message->getSound())) {
$payload['android_sound'] = $message->getSound();
$payload['ios_sound'] = $message->getSound() . '.wav';
}
if (!\is_null($message->getSound())) {
$payload['android_sound'] = $message->getSound();
$payload['ios_sound'] = $message->getSound();
}

if (!\is_null($message->getBadge())) {
$payload['ios_badgeType'] = 'SetTo';
$payload['ios_badgeCount'] = $message->getBadge();
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Comment thread
greptile-apps[bot] marked this conversation as resolved.
if (!\is_null($message->getContentAvailable())) {
$payload['content_available'] = true;
}
Comment on lines +84 to +86

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 content_available condition inverts intent when explicitly set to false

The guard !\is_null($message->getContentAvailable()) is true for both true and false. If a caller explicitly constructs Push with contentAvailable: false, the adapter still writes 'content_available' => true into the payload, silently enabling silent/background push on OneSignal devices when the caller's intent was the opposite. The fix is to check the boolean value directly: if ($message->getContentAvailable()).

if (!\is_null($message->getPriority())) {
$payload['priority'] = match ($message->getPriority()) {
Priority::HIGH => 10,
Priority::NORMAL => 5,
};
}

$results = $this->request(
method: 'POST',
url: 'https://onesignal.com/api/v1/notifications',
headers: [
'Content-Type: application/json',
"Authorization: Basic {$this->apiKey}",
],
body: $payload
);

$response = new Response($this->getType());

if ($results['statusCode'] >= 200 && $results['statusCode'] < 300) {
$response->setDeliveredTo(\count($message->getTo()));
foreach ($message->getTo() as $to) {
$response->addResult($to);
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
} else {
foreach ($message->getTo() as $to) {
$response->addResult($to, $results['response']['errors'][0] ?? 'Unknown error');
}
}

return $response->toArray();
}
}
23 changes: 23 additions & 0 deletions tests/Messaging/Adapter/Push/OneSignalTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Utopia\Tests\Adapter\Push;

use Utopia\Messaging\Adapter\Push\OneSignal as OneSignalAdapter;

class OneSignalTest extends Base
{
protected function setUp(): void
{
parent::setUp();

$appId = \getenv('ONESIGNAL_APP_ID');
$apiKey = \getenv('ONESIGNAL_API_KEY');

$this->adapter = new OneSignalAdapter($appId, $apiKey);
}

protected function getTo(): array
{
return [\getenv('ONESIGNAL_TO')];
}
}