diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9485214 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +## 1.9.0 + +### New Features + +- **New Feature: App Icon Badge Count** - Show MRR, user counts, stock prices, and more on your ActivitySmith app icon. diff --git a/README.md b/README.md index 48aa3ec..d4f8de5 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,9 @@ See [API reference](https://activitysmith.com/docs/api-reference/introduction). - [Live Activity Action](#live-activity-action) - [Icons and Badges](#icons-and-badges) - [Live Activity Colors](#live-activity-colors) -- [Channels](#channels) - [Widgets](#widgets) +- [App Icon Badge Count](#app-icon-badge-count) +- [Channels](#channels) ## Installation @@ -514,18 +515,6 @@ Choose from these colors for the Live Activity accent, including progress bars a `lime`, `green`, `cyan`, `blue`, `purple`, `magenta`, `red`, `orange`, `yellow`, `gray` -## Channels - -Channels are used to target specific team members or devices. Can be used for both push notifications and live activities. - -```php -$activitysmith->notifications->send( - title: 'New subscription 💸', - message: 'Customer upgraded to Pro plan', - channels: ['sales', 'customer-success'], // Optional -); -``` - ## Widgets

@@ -548,6 +537,58 @@ String metric values work too. $activitysmith->metrics->update('prod.status', 'healthy'); ``` +## App Icon Badge Count + +

+ ActivitySmith app icon with an App Icon Badge Count +

+ +Show the number you care about on your ActivitySmith app icon. Track MRR, a customer count, a stock price, or any other value you want to keep in view. + +Set or update the badge value. + +```php +$activitysmith->badgeCount(8333); +``` + +To clear the badge, set its value to 0. + +```php +$activitysmith->badgeCount(0); +``` + +## Channels + +Use `channels` to target specific team members or devices + +### Push Notifications + +```php +$activitysmith->notifications->send( + title: 'New subscription 💸', + message: 'Customer upgraded to Pro plan', + channels: ['sales', 'customer-success'], +); +``` + +### Live Activities + +```php +$activitysmith->liveActivities->start( + title: 'Nightly Database Backup', + subtitle: 'verify restore', + type: 'progress', + percentage: 62, + channels: ['sales', 'customer-success'], +); +``` + +### App Icon Badge Count + +```php +$activitysmith->badgeCount(3, channels: ['sales', 'customer-success']); +``` + ## Error Handling ```php diff --git a/generated/Api/AppIconBadgesApi.php b/generated/Api/AppIconBadgesApi.php new file mode 100644 index 0000000..1738c0d --- /dev/null +++ b/generated/Api/AppIconBadgesApi.php @@ -0,0 +1,593 @@ + [ + 'application/json', + ], + ]; + + /** + * @param ClientInterface $client + * @param Configuration $config + * @param HeaderSelector $selector + * @param int $hostIndex (Optional) host index to select the list of hosts if defined in the OpenAPI spec + */ + public function __construct( + ClientInterface $client = null, + Configuration $config = null, + HeaderSelector $selector = null, + $hostIndex = 0 + ) { + $this->client = $client ?: new Client(); + $this->config = $config ?: new Configuration(); + $this->headerSelector = $selector ?: new HeaderSelector(); + $this->hostIndex = $hostIndex; + } + + /** + * Set the host index + * + * @param int $hostIndex Host index (required) + */ + public function setHostIndex($hostIndex): void + { + $this->hostIndex = $hostIndex; + } + + /** + * Get the host index + * + * @return int Host index + */ + public function getHostIndex() + { + return $this->hostIndex; + } + + /** + * @return Configuration + */ + public function getConfig() + { + return $this->config; + } + + /** + * Operation updateAppIconBadgeCount + * + * Update App Icon Badge Count + * + * @param \ActivitySmith\Generated\Model\AppIconBadgeCountUpdateRequest $appIconBadgeCountUpdateRequest appIconBadgeCountUpdateRequest (required) + * @param string $contentType The value for the Content-Type header. Check self::contentTypes['updateAppIconBadgeCount'] to see the possible values for this operation + * + * @throws \ActivitySmith\Generated\ApiException on non-2xx response or if the response body is not in the expected format + * @throws \InvalidArgumentException + * @return \ActivitySmith\Generated\Model\AppIconBadgeCountUpdateResponse|\ActivitySmith\Generated\Model\BadRequestError|\ActivitySmith\Generated\Model\ForbiddenError|\ActivitySmith\Generated\Model\NoRecipientsError|\ActivitySmith\Generated\Model\RateLimitError + */ + public function updateAppIconBadgeCount($appIconBadgeCountUpdateRequest, string $contentType = self::contentTypes['updateAppIconBadgeCount'][0]) + { + list($response) = $this->updateAppIconBadgeCountWithHttpInfo($appIconBadgeCountUpdateRequest, $contentType); + return $response; + } + + /** + * Operation updateAppIconBadgeCountWithHttpInfo + * + * Update App Icon Badge Count + * + * @param \ActivitySmith\Generated\Model\AppIconBadgeCountUpdateRequest $appIconBadgeCountUpdateRequest (required) + * @param string $contentType The value for the Content-Type header. Check self::contentTypes['updateAppIconBadgeCount'] to see the possible values for this operation + * + * @throws \ActivitySmith\Generated\ApiException on non-2xx response or if the response body is not in the expected format + * @throws \InvalidArgumentException + * @return array of \ActivitySmith\Generated\Model\AppIconBadgeCountUpdateResponse|\ActivitySmith\Generated\Model\BadRequestError|\ActivitySmith\Generated\Model\ForbiddenError|\ActivitySmith\Generated\Model\NoRecipientsError|\ActivitySmith\Generated\Model\RateLimitError, HTTP status code, HTTP response headers (array of strings) + */ + public function updateAppIconBadgeCountWithHttpInfo($appIconBadgeCountUpdateRequest, string $contentType = self::contentTypes['updateAppIconBadgeCount'][0]) + { + $request = $this->updateAppIconBadgeCountRequest($appIconBadgeCountUpdateRequest, $contentType); + + try { + $options = $this->createHttpClientOption(); + try { + $response = $this->client->send($request, $options); + } catch (RequestException $e) { + throw new ApiException( + "[{$e->getCode()}] {$e->getMessage()}", + (int) $e->getCode(), + $e->getResponse() ? $e->getResponse()->getHeaders() : null, + $e->getResponse() ? (string) $e->getResponse()->getBody() : null + ); + } catch (ConnectException $e) { + throw new ApiException( + "[{$e->getCode()}] {$e->getMessage()}", + (int) $e->getCode(), + null, + null + ); + } + + $statusCode = $response->getStatusCode(); + + if ($statusCode < 200 || $statusCode > 299) { + throw new ApiException( + sprintf( + '[%d] Error connecting to the API (%s)', + $statusCode, + (string) $request->getUri() + ), + $statusCode, + $response->getHeaders(), + (string) $response->getBody() + ); + } + + switch($statusCode) { + case 200: + if ('\ActivitySmith\Generated\Model\AppIconBadgeCountUpdateResponse' === '\SplFileObject') { + $content = $response->getBody(); //stream goes to serializer + } else { + $content = (string) $response->getBody(); + if ('\ActivitySmith\Generated\Model\AppIconBadgeCountUpdateResponse' !== 'string') { + try { + $content = json_decode($content, false, 512, JSON_THROW_ON_ERROR); + } catch (\JsonException $exception) { + throw new ApiException( + sprintf( + 'Error JSON decoding server response (%s)', + $request->getUri() + ), + $statusCode, + $response->getHeaders(), + $content + ); + } + } + } + + return [ + ObjectSerializer::deserialize($content, '\ActivitySmith\Generated\Model\AppIconBadgeCountUpdateResponse', []), + $response->getStatusCode(), + $response->getHeaders() + ]; + case 400: + if ('\ActivitySmith\Generated\Model\BadRequestError' === '\SplFileObject') { + $content = $response->getBody(); //stream goes to serializer + } else { + $content = (string) $response->getBody(); + if ('\ActivitySmith\Generated\Model\BadRequestError' !== 'string') { + try { + $content = json_decode($content, false, 512, JSON_THROW_ON_ERROR); + } catch (\JsonException $exception) { + throw new ApiException( + sprintf( + 'Error JSON decoding server response (%s)', + $request->getUri() + ), + $statusCode, + $response->getHeaders(), + $content + ); + } + } + } + + return [ + ObjectSerializer::deserialize($content, '\ActivitySmith\Generated\Model\BadRequestError', []), + $response->getStatusCode(), + $response->getHeaders() + ]; + case 403: + if ('\ActivitySmith\Generated\Model\ForbiddenError' === '\SplFileObject') { + $content = $response->getBody(); //stream goes to serializer + } else { + $content = (string) $response->getBody(); + if ('\ActivitySmith\Generated\Model\ForbiddenError' !== 'string') { + try { + $content = json_decode($content, false, 512, JSON_THROW_ON_ERROR); + } catch (\JsonException $exception) { + throw new ApiException( + sprintf( + 'Error JSON decoding server response (%s)', + $request->getUri() + ), + $statusCode, + $response->getHeaders(), + $content + ); + } + } + } + + return [ + ObjectSerializer::deserialize($content, '\ActivitySmith\Generated\Model\ForbiddenError', []), + $response->getStatusCode(), + $response->getHeaders() + ]; + case 404: + if ('\ActivitySmith\Generated\Model\NoRecipientsError' === '\SplFileObject') { + $content = $response->getBody(); //stream goes to serializer + } else { + $content = (string) $response->getBody(); + if ('\ActivitySmith\Generated\Model\NoRecipientsError' !== 'string') { + try { + $content = json_decode($content, false, 512, JSON_THROW_ON_ERROR); + } catch (\JsonException $exception) { + throw new ApiException( + sprintf( + 'Error JSON decoding server response (%s)', + $request->getUri() + ), + $statusCode, + $response->getHeaders(), + $content + ); + } + } + } + + return [ + ObjectSerializer::deserialize($content, '\ActivitySmith\Generated\Model\NoRecipientsError', []), + $response->getStatusCode(), + $response->getHeaders() + ]; + case 429: + if ('\ActivitySmith\Generated\Model\RateLimitError' === '\SplFileObject') { + $content = $response->getBody(); //stream goes to serializer + } else { + $content = (string) $response->getBody(); + if ('\ActivitySmith\Generated\Model\RateLimitError' !== 'string') { + try { + $content = json_decode($content, false, 512, JSON_THROW_ON_ERROR); + } catch (\JsonException $exception) { + throw new ApiException( + sprintf( + 'Error JSON decoding server response (%s)', + $request->getUri() + ), + $statusCode, + $response->getHeaders(), + $content + ); + } + } + } + + return [ + ObjectSerializer::deserialize($content, '\ActivitySmith\Generated\Model\RateLimitError', []), + $response->getStatusCode(), + $response->getHeaders() + ]; + } + + $returnType = '\ActivitySmith\Generated\Model\AppIconBadgeCountUpdateResponse'; + if ($returnType === '\SplFileObject') { + $content = $response->getBody(); //stream goes to serializer + } else { + $content = (string) $response->getBody(); + if ($returnType !== 'string') { + try { + $content = json_decode($content, false, 512, JSON_THROW_ON_ERROR); + } catch (\JsonException $exception) { + throw new ApiException( + sprintf( + 'Error JSON decoding server response (%s)', + $request->getUri() + ), + $statusCode, + $response->getHeaders(), + $content + ); + } + } + } + + return [ + ObjectSerializer::deserialize($content, $returnType, []), + $response->getStatusCode(), + $response->getHeaders() + ]; + + } catch (ApiException $e) { + switch ($e->getCode()) { + case 200: + $data = ObjectSerializer::deserialize( + $e->getResponseBody(), + '\ActivitySmith\Generated\Model\AppIconBadgeCountUpdateResponse', + $e->getResponseHeaders() + ); + $e->setResponseObject($data); + break; + case 400: + $data = ObjectSerializer::deserialize( + $e->getResponseBody(), + '\ActivitySmith\Generated\Model\BadRequestError', + $e->getResponseHeaders() + ); + $e->setResponseObject($data); + break; + case 403: + $data = ObjectSerializer::deserialize( + $e->getResponseBody(), + '\ActivitySmith\Generated\Model\ForbiddenError', + $e->getResponseHeaders() + ); + $e->setResponseObject($data); + break; + case 404: + $data = ObjectSerializer::deserialize( + $e->getResponseBody(), + '\ActivitySmith\Generated\Model\NoRecipientsError', + $e->getResponseHeaders() + ); + $e->setResponseObject($data); + break; + case 429: + $data = ObjectSerializer::deserialize( + $e->getResponseBody(), + '\ActivitySmith\Generated\Model\RateLimitError', + $e->getResponseHeaders() + ); + $e->setResponseObject($data); + break; + } + throw $e; + } + } + + /** + * Operation updateAppIconBadgeCountAsync + * + * Update App Icon Badge Count + * + * @param \ActivitySmith\Generated\Model\AppIconBadgeCountUpdateRequest $appIconBadgeCountUpdateRequest (required) + * @param string $contentType The value for the Content-Type header. Check self::contentTypes['updateAppIconBadgeCount'] to see the possible values for this operation + * + * @throws \InvalidArgumentException + * @return \GuzzleHttp\Promise\PromiseInterface + */ + public function updateAppIconBadgeCountAsync($appIconBadgeCountUpdateRequest, string $contentType = self::contentTypes['updateAppIconBadgeCount'][0]) + { + return $this->updateAppIconBadgeCountAsyncWithHttpInfo($appIconBadgeCountUpdateRequest, $contentType) + ->then( + function ($response) { + return $response[0]; + } + ); + } + + /** + * Operation updateAppIconBadgeCountAsyncWithHttpInfo + * + * Update App Icon Badge Count + * + * @param \ActivitySmith\Generated\Model\AppIconBadgeCountUpdateRequest $appIconBadgeCountUpdateRequest (required) + * @param string $contentType The value for the Content-Type header. Check self::contentTypes['updateAppIconBadgeCount'] to see the possible values for this operation + * + * @throws \InvalidArgumentException + * @return \GuzzleHttp\Promise\PromiseInterface + */ + public function updateAppIconBadgeCountAsyncWithHttpInfo($appIconBadgeCountUpdateRequest, string $contentType = self::contentTypes['updateAppIconBadgeCount'][0]) + { + $returnType = '\ActivitySmith\Generated\Model\AppIconBadgeCountUpdateResponse'; + $request = $this->updateAppIconBadgeCountRequest($appIconBadgeCountUpdateRequest, $contentType); + + return $this->client + ->sendAsync($request, $this->createHttpClientOption()) + ->then( + function ($response) use ($returnType) { + if ($returnType === '\SplFileObject') { + $content = $response->getBody(); //stream goes to serializer + } else { + $content = (string) $response->getBody(); + if ($returnType !== 'string') { + $content = json_decode($content); + } + } + + return [ + ObjectSerializer::deserialize($content, $returnType, []), + $response->getStatusCode(), + $response->getHeaders() + ]; + }, + function ($exception) { + $response = $exception->getResponse(); + $statusCode = $response->getStatusCode(); + throw new ApiException( + sprintf( + '[%d] Error connecting to the API (%s)', + $statusCode, + $exception->getRequest()->getUri() + ), + $statusCode, + $response->getHeaders(), + (string) $response->getBody() + ); + } + ); + } + + /** + * Create request for operation 'updateAppIconBadgeCount' + * + * @param \ActivitySmith\Generated\Model\AppIconBadgeCountUpdateRequest $appIconBadgeCountUpdateRequest (required) + * @param string $contentType The value for the Content-Type header. Check self::contentTypes['updateAppIconBadgeCount'] to see the possible values for this operation + * + * @throws \InvalidArgumentException + * @return \GuzzleHttp\Psr7\Request + */ + public function updateAppIconBadgeCountRequest($appIconBadgeCountUpdateRequest, string $contentType = self::contentTypes['updateAppIconBadgeCount'][0]) + { + + // verify the required parameter 'appIconBadgeCountUpdateRequest' is set + if ($appIconBadgeCountUpdateRequest === null || (is_array($appIconBadgeCountUpdateRequest) && count($appIconBadgeCountUpdateRequest) === 0)) { + throw new \InvalidArgumentException( + 'Missing the required parameter $appIconBadgeCountUpdateRequest when calling updateAppIconBadgeCount' + ); + } + + + $resourcePath = '/badge'; + $formParams = []; + $queryParams = []; + $headerParams = []; + $httpBody = ''; + $multipart = false; + + + + + + $headers = $this->headerSelector->selectHeaders( + ['application/json', ], + $contentType, + $multipart + ); + + // for model (json/xml) + if (isset($appIconBadgeCountUpdateRequest)) { + if (stripos($headers['Content-Type'], 'application/json') !== false) { + # if Content-Type contains "application/json", json_encode the body + $httpBody = \GuzzleHttp\Utils::jsonEncode(ObjectSerializer::sanitizeForSerialization($appIconBadgeCountUpdateRequest)); + } else { + $httpBody = $appIconBadgeCountUpdateRequest; + } + } elseif (count($formParams) > 0) { + if ($multipart) { + $multipartContents = []; + foreach ($formParams as $formParamName => $formParamValue) { + $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue]; + foreach ($formParamValueItems as $formParamValueItem) { + $multipartContents[] = [ + 'name' => $formParamName, + 'contents' => $formParamValueItem + ]; + } + } + // for HTTP post (form) + $httpBody = new MultipartStream($multipartContents); + + } elseif (stripos($headers['Content-Type'], 'application/json') !== false) { + # if Content-Type contains "application/json", json_encode the form parameters + $httpBody = \GuzzleHttp\Utils::jsonEncode($formParams); + } else { + // for HTTP post (form) + $httpBody = ObjectSerializer::buildQuery($formParams); + } + } + + // this endpoint requires Bearer (API Key) authentication (access token) + if (!empty($this->config->getAccessToken())) { + $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken(); + } + + $defaultHeaders = []; + if ($this->config->getUserAgent()) { + $defaultHeaders['User-Agent'] = $this->config->getUserAgent(); + } + + $headers = array_merge( + $defaultHeaders, + $headerParams, + $headers + ); + + $operationHost = $this->config->getHost(); + $query = ObjectSerializer::buildQuery($queryParams); + return new Request( + 'POST', + $operationHost . $resourcePath . ($query ? "?{$query}" : ''), + $headers, + $httpBody + ); + } + + /** + * Create http client option + * + * @throws \RuntimeException on file opening failure + * @return array of http client options + */ + protected function createHttpClientOption() + { + $options = []; + if ($this->config->getDebug()) { + $options[RequestOptions::DEBUG] = fopen($this->config->getDebugFile(), 'a'); + if (!$options[RequestOptions::DEBUG]) { + throw new \RuntimeException('Failed to open the debug file: ' . $this->config->getDebugFile()); + } + } + + return $options; + } +} diff --git a/generated/Model/AppIconBadgeCountUpdateRequest.php b/generated/Model/AppIconBadgeCountUpdateRequest.php new file mode 100644 index 0000000..0d3d835 --- /dev/null +++ b/generated/Model/AppIconBadgeCountUpdateRequest.php @@ -0,0 +1,463 @@ + + */ +class AppIconBadgeCountUpdateRequest implements ModelInterface, ArrayAccess, \JsonSerializable +{ + public const DISCRIMINATOR = null; + + /** + * The original name of the model. + * + * @var string + */ + protected static $openAPIModelName = 'AppIconBadgeCountUpdateRequest'; + + /** + * Array of property to type mappings. Used for (de)serialization + * + * @var string[] + */ + protected static $openAPITypes = [ + 'badge' => 'int', + 'target' => '\ActivitySmith\Generated\Model\ChannelTarget' + ]; + + /** + * Array of property to format mappings. Used for (de)serialization + * + * @var string[] + * @phpstan-var array + * @psalm-var array + */ + protected static $openAPIFormats = [ + 'badge' => null, + 'target' => null + ]; + + /** + * Array of nullable properties. Used for (de)serialization + * + * @var boolean[] + */ + protected static array $openAPINullables = [ + 'badge' => false, + 'target' => false + ]; + + /** + * If a nullable field gets set to null, insert it here + * + * @var boolean[] + */ + protected array $openAPINullablesSetToNull = []; + + /** + * Array of property to type mappings. Used for (de)serialization + * + * @return array + */ + public static function openAPITypes() + { + return self::$openAPITypes; + } + + /** + * Array of property to format mappings. Used for (de)serialization + * + * @return array + */ + public static function openAPIFormats() + { + return self::$openAPIFormats; + } + + /** + * Array of nullable properties + * + * @return array + */ + protected static function openAPINullables(): array + { + return self::$openAPINullables; + } + + /** + * Array of nullable field names deliberately set to null + * + * @return boolean[] + */ + private function getOpenAPINullablesSetToNull(): array + { + return $this->openAPINullablesSetToNull; + } + + /** + * Setter - Array of nullable field names deliberately set to null + * + * @param boolean[] $openAPINullablesSetToNull + */ + private function setOpenAPINullablesSetToNull(array $openAPINullablesSetToNull): void + { + $this->openAPINullablesSetToNull = $openAPINullablesSetToNull; + } + + /** + * Checks if a property is nullable + * + * @param string $property + * @return bool + */ + public static function isNullable(string $property): bool + { + return self::openAPINullables()[$property] ?? false; + } + + /** + * Checks if a nullable property is set to null. + * + * @param string $property + * @return bool + */ + public function isNullableSetToNull(string $property): bool + { + return in_array($property, $this->getOpenAPINullablesSetToNull(), true); + } + + /** + * Array of attributes where the key is the local name, + * and the value is the original name + * + * @var string[] + */ + protected static $attributeMap = [ + 'badge' => 'badge', + 'target' => 'target' + ]; + + /** + * Array of attributes to setter functions (for deserialization of responses) + * + * @var string[] + */ + protected static $setters = [ + 'badge' => 'setBadge', + 'target' => 'setTarget' + ]; + + /** + * Array of attributes to getter functions (for serialization of requests) + * + * @var string[] + */ + protected static $getters = [ + 'badge' => 'getBadge', + 'target' => 'getTarget' + ]; + + /** + * Array of attributes where the key is the local name, + * and the value is the original name + * + * @return array + */ + public static function attributeMap() + { + return self::$attributeMap; + } + + /** + * Array of attributes to setter functions (for deserialization of responses) + * + * @return array + */ + public static function setters() + { + return self::$setters; + } + + /** + * Array of attributes to getter functions (for serialization of requests) + * + * @return array + */ + public static function getters() + { + return self::$getters; + } + + /** + * The original name of the model. + * + * @return string + */ + public function getModelName() + { + return self::$openAPIModelName; + } + + + /** + * Associative array for storing property values + * + * @var mixed[] + */ + protected $container = []; + + /** + * Constructor + * + * @param mixed[] $data Associated array of property values + * initializing the model + */ + public function __construct(array $data = null) + { + $this->setIfExists('badge', $data ?? [], null); + $this->setIfExists('target', $data ?? [], null); + } + + /** + * Sets $this->container[$variableName] to the given data or to the given default Value; if $variableName + * is nullable and its value is set to null in the $fields array, then mark it as "set to null" in the + * $this->openAPINullablesSetToNull array + * + * @param string $variableName + * @param array $fields + * @param mixed $defaultValue + */ + private function setIfExists(string $variableName, array $fields, $defaultValue): void + { + if (self::isNullable($variableName) && array_key_exists($variableName, $fields) && is_null($fields[$variableName])) { + $this->openAPINullablesSetToNull[] = $variableName; + } + + $this->container[$variableName] = $fields[$variableName] ?? $defaultValue; + } + + /** + * Show all the invalid properties with reasons. + * + * @return array invalid properties with reasons + */ + public function listInvalidProperties() + { + $invalidProperties = []; + + if ($this->container['badge'] === null) { + $invalidProperties[] = "'badge' can't be null"; + } + if (($this->container['badge'] > 2147483647)) { + $invalidProperties[] = "invalid value for 'badge', must be smaller than or equal to 2147483647."; + } + + if (($this->container['badge'] < 0)) { + $invalidProperties[] = "invalid value for 'badge', must be bigger than or equal to 0."; + } + + return $invalidProperties; + } + + /** + * Validate all the properties in the model + * return true if all passed + * + * @return bool True if all properties are valid + */ + public function valid() + { + return count($this->listInvalidProperties()) === 0; + } + + + /** + * Gets badge + * + * @return int + */ + public function getBadge() + { + return $this->container['badge']; + } + + /** + * Sets badge + * + * @param int $badge The count to show on the ActivitySmith app icon. Send 0 to clear it. + * + * @return self + */ + public function setBadge($badge) + { + if (is_null($badge)) { + throw new \InvalidArgumentException('non-nullable badge cannot be null'); + } + + if (($badge > 2147483647)) { + throw new \InvalidArgumentException('invalid value for $badge when calling AppIconBadgeCountUpdateRequest., must be smaller than or equal to 2147483647.'); + } + if (($badge < 0)) { + throw new \InvalidArgumentException('invalid value for $badge when calling AppIconBadgeCountUpdateRequest., must be bigger than or equal to 0.'); + } + + $this->container['badge'] = $badge; + + return $this; + } + + /** + * Gets target + * + * @return \ActivitySmith\Generated\Model\ChannelTarget|null + */ + public function getTarget() + { + return $this->container['target']; + } + + /** + * Sets target + * + * @param \ActivitySmith\Generated\Model\ChannelTarget|null $target target + * + * @return self + */ + public function setTarget($target) + { + if (is_null($target)) { + throw new \InvalidArgumentException('non-nullable target cannot be null'); + } + $this->container['target'] = $target; + + return $this; + } + /** + * Returns true if offset exists. False otherwise. + * + * @param integer $offset Offset + * + * @return boolean + */ + public function offsetExists($offset): bool + { + return isset($this->container[$offset]); + } + + /** + * Gets offset. + * + * @param integer $offset Offset + * + * @return mixed|null + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + return $this->container[$offset] ?? null; + } + + /** + * Sets value based on offset. + * + * @param int|null $offset Offset + * @param mixed $value Value to be set + * + * @return void + */ + public function offsetSet($offset, $value): void + { + if (is_null($offset)) { + $this->container[] = $value; + } else { + $this->container[$offset] = $value; + } + } + + /** + * Unsets offset. + * + * @param integer $offset Offset + * + * @return void + */ + public function offsetUnset($offset): void + { + unset($this->container[$offset]); + } + + /** + * Serializes the object to a value that can be serialized natively by json_encode(). + * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php + * + * @return mixed Returns data which can be serialized by json_encode(), which is a value + * of any type other than a resource. + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return ObjectSerializer::sanitizeForSerialization($this); + } + + /** + * Gets the string presentation of the object + * + * @return string + */ + public function __toString() + { + return json_encode( + ObjectSerializer::sanitizeForSerialization($this), + JSON_PRETTY_PRINT + ); + } + + /** + * Gets a header-safe presentation of the object + * + * @return string + */ + public function toHeaderValue() + { + return json_encode(ObjectSerializer::sanitizeForSerialization($this)); + } +} + + diff --git a/generated/Model/AppIconBadgeCountUpdateResponse.php b/generated/Model/AppIconBadgeCountUpdateResponse.php new file mode 100644 index 0000000..96a06e4 --- /dev/null +++ b/generated/Model/AppIconBadgeCountUpdateResponse.php @@ -0,0 +1,613 @@ + + */ +class AppIconBadgeCountUpdateResponse implements ModelInterface, ArrayAccess, \JsonSerializable +{ + public const DISCRIMINATOR = null; + + /** + * The original name of the model. + * + * @var string + */ + protected static $openAPIModelName = 'AppIconBadgeCountUpdateResponse'; + + /** + * Array of property to type mappings. Used for (de)serialization + * + * @var string[] + */ + protected static $openAPITypes = [ + 'success' => 'bool', + 'badge' => 'int', + 'devicesNotified' => 'int', + 'usersNotified' => 'int', + 'effectiveChannelSlugs' => 'string[]', + 'timestamp' => '\DateTime' + ]; + + /** + * Array of property to format mappings. Used for (de)serialization + * + * @var string[] + * @phpstan-var array + * @psalm-var array + */ + protected static $openAPIFormats = [ + 'success' => null, + 'badge' => null, + 'devicesNotified' => null, + 'usersNotified' => null, + 'effectiveChannelSlugs' => null, + 'timestamp' => 'date-time' + ]; + + /** + * Array of nullable properties. Used for (de)serialization + * + * @var boolean[] + */ + protected static array $openAPINullables = [ + 'success' => false, + 'badge' => false, + 'devicesNotified' => false, + 'usersNotified' => false, + 'effectiveChannelSlugs' => false, + 'timestamp' => false + ]; + + /** + * If a nullable field gets set to null, insert it here + * + * @var boolean[] + */ + protected array $openAPINullablesSetToNull = []; + + /** + * Array of property to type mappings. Used for (de)serialization + * + * @return array + */ + public static function openAPITypes() + { + return self::$openAPITypes; + } + + /** + * Array of property to format mappings. Used for (de)serialization + * + * @return array + */ + public static function openAPIFormats() + { + return self::$openAPIFormats; + } + + /** + * Array of nullable properties + * + * @return array + */ + protected static function openAPINullables(): array + { + return self::$openAPINullables; + } + + /** + * Array of nullable field names deliberately set to null + * + * @return boolean[] + */ + private function getOpenAPINullablesSetToNull(): array + { + return $this->openAPINullablesSetToNull; + } + + /** + * Setter - Array of nullable field names deliberately set to null + * + * @param boolean[] $openAPINullablesSetToNull + */ + private function setOpenAPINullablesSetToNull(array $openAPINullablesSetToNull): void + { + $this->openAPINullablesSetToNull = $openAPINullablesSetToNull; + } + + /** + * Checks if a property is nullable + * + * @param string $property + * @return bool + */ + public static function isNullable(string $property): bool + { + return self::openAPINullables()[$property] ?? false; + } + + /** + * Checks if a nullable property is set to null. + * + * @param string $property + * @return bool + */ + public function isNullableSetToNull(string $property): bool + { + return in_array($property, $this->getOpenAPINullablesSetToNull(), true); + } + + /** + * Array of attributes where the key is the local name, + * and the value is the original name + * + * @var string[] + */ + protected static $attributeMap = [ + 'success' => 'success', + 'badge' => 'badge', + 'devicesNotified' => 'devices_notified', + 'usersNotified' => 'users_notified', + 'effectiveChannelSlugs' => 'effective_channel_slugs', + 'timestamp' => 'timestamp' + ]; + + /** + * Array of attributes to setter functions (for deserialization of responses) + * + * @var string[] + */ + protected static $setters = [ + 'success' => 'setSuccess', + 'badge' => 'setBadge', + 'devicesNotified' => 'setDevicesNotified', + 'usersNotified' => 'setUsersNotified', + 'effectiveChannelSlugs' => 'setEffectiveChannelSlugs', + 'timestamp' => 'setTimestamp' + ]; + + /** + * Array of attributes to getter functions (for serialization of requests) + * + * @var string[] + */ + protected static $getters = [ + 'success' => 'getSuccess', + 'badge' => 'getBadge', + 'devicesNotified' => 'getDevicesNotified', + 'usersNotified' => 'getUsersNotified', + 'effectiveChannelSlugs' => 'getEffectiveChannelSlugs', + 'timestamp' => 'getTimestamp' + ]; + + /** + * Array of attributes where the key is the local name, + * and the value is the original name + * + * @return array + */ + public static function attributeMap() + { + return self::$attributeMap; + } + + /** + * Array of attributes to setter functions (for deserialization of responses) + * + * @return array + */ + public static function setters() + { + return self::$setters; + } + + /** + * Array of attributes to getter functions (for serialization of requests) + * + * @return array + */ + public static function getters() + { + return self::$getters; + } + + /** + * The original name of the model. + * + * @return string + */ + public function getModelName() + { + return self::$openAPIModelName; + } + + + /** + * Associative array for storing property values + * + * @var mixed[] + */ + protected $container = []; + + /** + * Constructor + * + * @param mixed[] $data Associated array of property values + * initializing the model + */ + public function __construct(array $data = null) + { + $this->setIfExists('success', $data ?? [], null); + $this->setIfExists('badge', $data ?? [], null); + $this->setIfExists('devicesNotified', $data ?? [], null); + $this->setIfExists('usersNotified', $data ?? [], null); + $this->setIfExists('effectiveChannelSlugs', $data ?? [], null); + $this->setIfExists('timestamp', $data ?? [], null); + } + + /** + * Sets $this->container[$variableName] to the given data or to the given default Value; if $variableName + * is nullable and its value is set to null in the $fields array, then mark it as "set to null" in the + * $this->openAPINullablesSetToNull array + * + * @param string $variableName + * @param array $fields + * @param mixed $defaultValue + */ + private function setIfExists(string $variableName, array $fields, $defaultValue): void + { + if (self::isNullable($variableName) && array_key_exists($variableName, $fields) && is_null($fields[$variableName])) { + $this->openAPINullablesSetToNull[] = $variableName; + } + + $this->container[$variableName] = $fields[$variableName] ?? $defaultValue; + } + + /** + * Show all the invalid properties with reasons. + * + * @return array invalid properties with reasons + */ + public function listInvalidProperties() + { + $invalidProperties = []; + + if ($this->container['success'] === null) { + $invalidProperties[] = "'success' can't be null"; + } + if ($this->container['badge'] === null) { + $invalidProperties[] = "'badge' can't be null"; + } + if (($this->container['badge'] > 2147483647)) { + $invalidProperties[] = "invalid value for 'badge', must be smaller than or equal to 2147483647."; + } + + if (($this->container['badge'] < 0)) { + $invalidProperties[] = "invalid value for 'badge', must be bigger than or equal to 0."; + } + + if ($this->container['devicesNotified'] === null) { + $invalidProperties[] = "'devicesNotified' can't be null"; + } + if ($this->container['usersNotified'] === null) { + $invalidProperties[] = "'usersNotified' can't be null"; + } + if ($this->container['effectiveChannelSlugs'] === null) { + $invalidProperties[] = "'effectiveChannelSlugs' can't be null"; + } + if ($this->container['timestamp'] === null) { + $invalidProperties[] = "'timestamp' can't be null"; + } + return $invalidProperties; + } + + /** + * Validate all the properties in the model + * return true if all passed + * + * @return bool True if all properties are valid + */ + public function valid() + { + return count($this->listInvalidProperties()) === 0; + } + + + /** + * Gets success + * + * @return bool + */ + public function getSuccess() + { + return $this->container['success']; + } + + /** + * Sets success + * + * @param bool $success success + * + * @return self + */ + public function setSuccess($success) + { + if (is_null($success)) { + throw new \InvalidArgumentException('non-nullable success cannot be null'); + } + $this->container['success'] = $success; + + return $this; + } + + /** + * Gets badge + * + * @return int + */ + public function getBadge() + { + return $this->container['badge']; + } + + /** + * Sets badge + * + * @param int $badge badge + * + * @return self + */ + public function setBadge($badge) + { + if (is_null($badge)) { + throw new \InvalidArgumentException('non-nullable badge cannot be null'); + } + + if (($badge > 2147483647)) { + throw new \InvalidArgumentException('invalid value for $badge when calling AppIconBadgeCountUpdateResponse., must be smaller than or equal to 2147483647.'); + } + if (($badge < 0)) { + throw new \InvalidArgumentException('invalid value for $badge when calling AppIconBadgeCountUpdateResponse., must be bigger than or equal to 0.'); + } + + $this->container['badge'] = $badge; + + return $this; + } + + /** + * Gets devicesNotified + * + * @return int + */ + public function getDevicesNotified() + { + return $this->container['devicesNotified']; + } + + /** + * Sets devicesNotified + * + * @param int $devicesNotified devicesNotified + * + * @return self + */ + public function setDevicesNotified($devicesNotified) + { + if (is_null($devicesNotified)) { + throw new \InvalidArgumentException('non-nullable devicesNotified cannot be null'); + } + $this->container['devicesNotified'] = $devicesNotified; + + return $this; + } + + /** + * Gets usersNotified + * + * @return int + */ + public function getUsersNotified() + { + return $this->container['usersNotified']; + } + + /** + * Sets usersNotified + * + * @param int $usersNotified usersNotified + * + * @return self + */ + public function setUsersNotified($usersNotified) + { + if (is_null($usersNotified)) { + throw new \InvalidArgumentException('non-nullable usersNotified cannot be null'); + } + $this->container['usersNotified'] = $usersNotified; + + return $this; + } + + /** + * Gets effectiveChannelSlugs + * + * @return string[] + */ + public function getEffectiveChannelSlugs() + { + return $this->container['effectiveChannelSlugs']; + } + + /** + * Sets effectiveChannelSlugs + * + * @param string[] $effectiveChannelSlugs effectiveChannelSlugs + * + * @return self + */ + public function setEffectiveChannelSlugs($effectiveChannelSlugs) + { + if (is_null($effectiveChannelSlugs)) { + throw new \InvalidArgumentException('non-nullable effectiveChannelSlugs cannot be null'); + } + $this->container['effectiveChannelSlugs'] = $effectiveChannelSlugs; + + return $this; + } + + /** + * Gets timestamp + * + * @return \DateTime + */ + public function getTimestamp() + { + return $this->container['timestamp']; + } + + /** + * Sets timestamp + * + * @param \DateTime $timestamp timestamp + * + * @return self + */ + public function setTimestamp($timestamp) + { + if (is_null($timestamp)) { + throw new \InvalidArgumentException('non-nullable timestamp cannot be null'); + } + $this->container['timestamp'] = $timestamp; + + return $this; + } + /** + * Returns true if offset exists. False otherwise. + * + * @param integer $offset Offset + * + * @return boolean + */ + public function offsetExists($offset): bool + { + return isset($this->container[$offset]); + } + + /** + * Gets offset. + * + * @param integer $offset Offset + * + * @return mixed|null + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + return $this->container[$offset] ?? null; + } + + /** + * Sets value based on offset. + * + * @param int|null $offset Offset + * @param mixed $value Value to be set + * + * @return void + */ + public function offsetSet($offset, $value): void + { + if (is_null($offset)) { + $this->container[] = $value; + } else { + $this->container[$offset] = $value; + } + } + + /** + * Unsets offset. + * + * @param integer $offset Offset + * + * @return void + */ + public function offsetUnset($offset): void + { + unset($this->container[$offset]); + } + + /** + * Serializes the object to a value that can be serialized natively by json_encode(). + * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php + * + * @return mixed Returns data which can be serialized by json_encode(), which is a value + * of any type other than a resource. + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return ObjectSerializer::sanitizeForSerialization($this); + } + + /** + * Gets the string presentation of the object + * + * @return string + */ + public function __toString() + { + return json_encode( + ObjectSerializer::sanitizeForSerialization($this), + JSON_PRETTY_PRINT + ); + } + + /** + * Gets a header-safe presentation of the object + * + * @return string + */ + public function toHeaderValue() + { + return json_encode(ObjectSerializer::sanitizeForSerialization($this)); + } +} + + diff --git a/generated/Model/LiveActivityAction.php b/generated/Model/LiveActivityAction.php index ec0aac6..cc8b434 100644 --- a/generated/Model/LiveActivityAction.php +++ b/generated/Model/LiveActivityAction.php @@ -62,7 +62,7 @@ class LiveActivityAction implements ModelInterface, ArrayAccess, \JsonSerializab 'type' => '\ActivitySmith\Generated\Model\LiveActivityActionType', 'url' => 'string', 'method' => '\ActivitySmith\Generated\Model\LiveActivityWebhookMethod', - 'body' => 'array' + 'body' => 'object' ]; /** @@ -438,7 +438,7 @@ public function setMethod($method) /** * Gets body * - * @return array|null + * @return object|null */ public function getBody() { @@ -448,7 +448,7 @@ public function getBody() /** * Sets body * - * @param array|null $body Optional webhook payload body. Used only when type=webhook. + * @param object|null $body Optional webhook payload body. Used only when type=webhook. * * @return self */ diff --git a/generated/Model/PushNotificationAction.php b/generated/Model/PushNotificationAction.php index 72d65a6..8996e3d 100644 --- a/generated/Model/PushNotificationAction.php +++ b/generated/Model/PushNotificationAction.php @@ -61,7 +61,7 @@ class PushNotificationAction implements ModelInterface, ArrayAccess, \JsonSerial 'type' => '\ActivitySmith\Generated\Model\PushNotificationActionType', 'url' => 'string', 'method' => '\ActivitySmith\Generated\Model\PushNotificationWebhookMethod', - 'body' => 'array' + 'body' => 'object' ]; /** @@ -437,7 +437,7 @@ public function setMethod($method) /** * Gets body * - * @return array|null + * @return object|null */ public function getBody() { @@ -447,7 +447,7 @@ public function getBody() /** * Sets body * - * @param array|null $body Optional webhook payload body. Used only when type=webhook. + * @param object|null $body Optional webhook payload body. Used only when type=webhook. * * @return self */ diff --git a/generated/Model/PushNotificationRequest.php b/generated/Model/PushNotificationRequest.php index 33bd6d9..ccf6900 100644 --- a/generated/Model/PushNotificationRequest.php +++ b/generated/Model/PushNotificationRequest.php @@ -63,7 +63,7 @@ class PushNotificationRequest implements ModelInterface, ArrayAccess, \JsonSeria 'media' => 'string', 'redirection' => 'string', 'actions' => '\ActivitySmith\Generated\Model\PushNotificationAction[]', - 'payload' => 'array', + 'payload' => 'object', 'badge' => 'int', 'sound' => 'string', 'target' => '\ActivitySmith\Generated\Model\ChannelTarget' @@ -546,7 +546,7 @@ public function setActions($actions) /** * Gets payload * - * @return array|null + * @return object|null */ public function getPayload() { @@ -556,7 +556,7 @@ public function getPayload() /** * Sets payload * - * @param array|null $payload payload + * @param object|null $payload payload * * @return self */ diff --git a/src/ActivitySmith.php b/src/ActivitySmith.php index ec74232..8dd4fd8 100644 --- a/src/ActivitySmith.php +++ b/src/ActivitySmith.php @@ -16,6 +16,7 @@ final class ActivitySmith private const PUSH_API_CLASS = 'ActivitySmith\\Generated\\Api\\PushNotificationsApi'; private const LIVE_API_CLASS = 'ActivitySmith\\Generated\\Api\\LiveActivitiesApi'; private const METRICS_API_CLASS = 'ActivitySmith\\Generated\\Api\\MetricsApi'; + private const APP_ICON_BADGES_API_CLASS = 'ActivitySmith\\Generated\\Api\\AppIconBadgesApi'; private const SDK_HEADER_NAME = 'X-ActivitySmith-SDK'; /** @var Notifications */ @@ -27,6 +28,8 @@ final class ActivitySmith /** @var Metrics */ public $metrics; + private $appIconBadges; + public function __construct(string $apiKey) { if ($apiKey === '') { @@ -39,6 +42,7 @@ public function __construct(string $apiKey) $pushApiClass = self::PUSH_API_CLASS; $liveApiClass = self::LIVE_API_CLASS; $metricsApiClass = self::METRICS_API_CLASS; + $appIconBadgesApiClass = self::APP_ICON_BADGES_API_CLASS; $configuration = $configurationClass::getDefaultConfiguration(); $configuration->setAccessToken($apiKey); @@ -56,6 +60,21 @@ public function __construct(string $apiKey) $this->notifications = new Notifications(new $pushApiClass($httpClient, $configuration)); $this->liveActivities = new LiveActivities(new $liveApiClass($httpClient, $configuration)); $this->metrics = new Metrics(new $metricsApiClass($httpClient, $configuration)); + $this->appIconBadges = new $appIconBadgesApiClass($httpClient, $configuration); + } + + /** + * @param array|string|null $channels + */ + public function badgeCount(int $value, array|string|null $channels = null): mixed + { + $request = ['badge' => $value]; + $channels = $this->normalizeChannels($channels); + if ($channels !== []) { + $request['target'] = ['channels' => $channels]; + } + + return $this->appIconBadges->updateAppIconBadgeCount($request); } private function assertGeneratedClientIsPresent(): void @@ -65,10 +84,33 @@ private function assertGeneratedClientIsPresent(): void || !class_exists(self::PUSH_API_CLASS) || !class_exists(self::LIVE_API_CLASS) || !class_exists(self::METRICS_API_CLASS) + || !class_exists(self::APP_ICON_BADGES_API_CLASS) ) { throw new RuntimeException( 'Generated PHP client not found. Run SDK regeneration so /generated contains OpenAPI output.' ); } } + + /** + * @param array|string|null $channels + * @return array + */ + private function normalizeChannels(array|string|null $channels): array + { + if (is_string($channels)) { + $channels = explode(',', $channels); + } + + if (!is_array($channels)) { + return []; + } + + return array_values( + array_filter( + array_map('trim', $channels), + static fn (string $channel): bool => $channel !== '' + ) + ); + } } diff --git a/src/Version.php b/src/Version.php index 074439d..d0b7daf 100644 --- a/src/Version.php +++ b/src/Version.php @@ -6,7 +6,7 @@ final class Version { - public const VERSION = '1.8.0'; + public const VERSION = '1.9.0'; private function __construct() { diff --git a/tests/ResourcesTest.php b/tests/ResourcesTest.php index 9d6681e..0c29860 100644 --- a/tests/ResourcesTest.php +++ b/tests/ResourcesTest.php @@ -5,6 +5,7 @@ namespace ActivitySmith\Tests; use ActivitySmith\LiveActivities; +use ActivitySmith\ActivitySmith; use ActivitySmith\LiveActivityAction; use ActivitySmith\LiveActivityAlertBadge; use ActivitySmith\LiveActivityAlertIcon; @@ -14,6 +15,7 @@ use ActivitySmith\Notifications; use ActivitySmith\PushAction; use ActivitySmith\Generated\Api\LiveActivitiesApi; +use ActivitySmith\Generated\Api\AppIconBadgesApi; use ActivitySmith\Generated\Api\MetricsApi; use ActivitySmith\Generated\Api\PushNotificationsApi; use ActivitySmith\Generated\Model\LiveActivityAction as GeneratedLiveActivityAction; @@ -25,6 +27,43 @@ final class ResourcesTest extends TestCase { + public function testBadgeCountClearsAndTargetsChannels(): void + { + $client = new ActivitySmith('test'); + $captured = []; + $api = $this->getMockBuilder(AppIconBadgesApi::class) + ->disableOriginalConstructor() + ->onlyMethods(['updateAppIconBadgeCount']) + ->getMock(); + $api->method('updateAppIconBadgeCount') + ->willReturnCallback(static function (mixed $request) use (&$captured): mixed { + $captured[] = $request; + return $request; + }); + + $property = new \ReflectionProperty(ActivitySmith::class, 'appIconBadges'); + $property->setValue($client, $api); + + self::assertSame(['badge' => 0], $client->badgeCount(0)); + self::assertSame( + [ + 'badge' => 3, + 'target' => ['channels' => ['sales', 'customer-success']], + ], + $client->badgeCount(3, 'sales,customer-success') + ); + self::assertSame( + [ + ['badge' => 0], + [ + 'badge' => 3, + 'target' => ['channels' => ['sales', 'customer-success']], + ], + ], + $captured + ); + } + public function testNotificationsShortAndLegacyMethods(): void { $payload = ['title' => 'Build Failed']; diff --git a/tests/SmokeTest.php b/tests/SmokeTest.php index 95097e2..51d9026 100644 --- a/tests/SmokeTest.php +++ b/tests/SmokeTest.php @@ -28,6 +28,6 @@ public function testClientConstructsWhenGeneratedCodeIsPresent(): void $this->assertTrue(method_exists($client->liveActivities, 'endStream')); $this->assertNotNull($client->metrics); $this->assertTrue(method_exists($client->metrics, 'update')); - $this->assertSame('1.8.0', Version::VERSION); + $this->assertSame('1.9.0', Version::VERSION); } }