# Anikeen ID [![Latest Stable Version](https://img.shields.io/packagist/v/anikeen/id.svg?style=flat-square)](https://packagist.org/packages/anikeen/id) [![Total Downloads](https://img.shields.io/packagist/dt/anikeen/id.svg?style=flat-square)](https://packagist.org/packages/anikeen/id) [![License](https://img.shields.io/packagist/l/anikeen/id.svg?style=flat-square)](https://packagist.org/packages/anikeen/id) PHP Anikeen ID API Client for Laravel 11+ ## Table of contents 1. [Installation](#installation) 2. [Configuration](#configuration) 3. [General](#general) 4. [Examples](#examples) 5. [Documentation](#documentation) 6. [Development](#Development) ## Installation ``` composer require anikeen/id ``` ## Configuration Add environmental variables to your `.env` file: ``` ANIKEEN_ID_KEY= ANIKEEN_ID_SECRET= ANIKEEN_ID_CALLBACK_URL=http://localhost/auth/callback ``` To switch from `production` to `staging` use following variable: ``` ANIKEEN_ID_MODE=staging ``` You will need to add an entry to the services configuration file so that after config files are cached for usage in production environment (Laravel command `artisan config:cache`) all config is still available. Add to `config/services.php` file: ```php 'anikeen' => [ 'mode' => env('ANIKEEN_ID_MODE'), 'client_id' => env('ANIKEEN_ID_KEY'), 'client_secret' => env('ANIKEEN_ID_SECRET'), 'redirect' => env('ANIKEEN_ID_CALLBACK_URL'), 'base_url' => env('ANIKEEN_ID_BASE_URL'), ], ``` ### Event Listener In Laravel 11, the default EventServiceProvider provider was removed. Instead, add the listener using the listen method on the Event facade, in your `AppServiceProvider` boot method: ```php public function boot(): void { Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) { $event->extendSocialite('anikeen', \Anikeen\Id\Socialite\Provider::class); }); } ``` ### Registering Middleware Append it to the global middleware stack in your application's `bootstrap/app.php` file: ```php ->withMiddleware(function (Middleware $middleware) { $middleware->web(append: [ \Anikeen\Id\Http\Middleware\CreateFreshApiToken::class, ]); }) ``` ### Implementing Billable To implement the `Billable` trait, you need to add the `Billable` trait to your user model. ```php use Anikeen\Id\Billable; class User extends Authenticatable { use Billable; // Your model code... } ``` then, you can use the `Billable` trait methods in your user model. ### App subscriptions (trusted apps, no user token) `subscriptions()` acts with the user's own access token and stops working when the user's login expired. Trusted apps can manage a user's subscriptions with the app's own token (client credentials, scope `subscriptions:client`) instead: ```php $subscriptions = $team->owner->appSubscriptions(); // nothing is requested yet $subscription = $subscriptions->create([ 'group' => $team->id, 'name' => 'Cloud Server S', 'unit' => 'hour', 'price' => 0.0137, 'idempotency_key' => "server:{$pendingServer->id}", // retries return the same subscription ]); $subscription->checkout(); // bill the first period $subscription->checkout(startsAt: $domain->registered_at); // already running: bill the period that started then $subscription->pause(); // expires when the paid period ends $subscription->resume(); // expired subscriptions start a new period $subscription->revoke(refund: false); // cancel $subscriptions->find($id); // null if unknown or not this user's $subscriptions->all(['group' => $team->id, 'active' => true]); ``` The user's Anikeen ID is read from the billable's `anikeen_id` attribute (`AnikeenId::useUserIdField('…')` to change it). The app must be trusted by Anikeen ID; it sees the subscriptions it created (plus those of clients delegated to it). Failures throw `Anikeen\Id\Exceptions\ApiException` with `status` (0 = not reachable), a stable `error` code (`already_active`, `already_cancelled`, `not_active`, `already_paused`, `already_resumed`, `billing_address_missing`, `not_changeable`, `subscription_not_found`, `idempotency_key_conflict`) and `isNotFound()`, `isConflict()`, `isTemporary()`: ```php try { $subscription->revoke(); } catch (ApiException $e) { if ($e->isNotFound() || $e->error === 'already_cancelled') { // already gone } elseif ($e->isTemporary()) { // retry later } else { throw $e; } } ``` ### App orders (trusted apps, no user token) Orders that bill the app's subscriptions, read with the app's token (scope `orders:client`): ```php $team->owner->appOrders()->pending(); // unpaid orders of one user, oldest first app(AnikeenId::class)->appOrders()->pending(); // of all users $team->owner->appOrders()->find($orderId); // null if unknown or not this user's ``` Each order has `status` (`processing` = current month, still collecting; `pending` = closed and not paid; `completed`; `cancelled`), the totals, `payment_attempts`, `next_payment_attempt_at` (null = no more automatic retries) and `last_payment_error`. ### Selling as a third-party app (user confirms on Anikeen ID) Apps that are not trusted can create orders and subscriptions for the signed-in user (user token), but can't charge them. The user confirms on Anikeen ID and comes back to your `return_url`: ```php use Anikeen\Id\Exceptions\ConfirmationRequiredException; $order = $user->orders()->create([ 'billing_address_id' => $addressId, 'items' => [['type' => 'digital', 'name' => 'Sticker pack', 'unit' => 'piece', 'units' => 1, 'gross_price' => '9.99']], ]); return redirect($order->confirmationUrl(route('shop.done'))); // later: https://your-app.test/shop/done?status=completed&order=01J… ``` `return_url` must have the origin of one of your app's redirect URIs. `status` is `completed`, `pending` (payment still processing) or `cancelled`; check it with `$user->orders()->find($id)` instead of trusting the query string. Subscriptions work the same way (`$user->subscriptions()->create([...])->confirmationUrl(...)`, `&subscription=…`). Unconfirmed bookings are cancelled after 24 hours. Physical goods need a shipping address (`first_name`, `last_name`, `address`, `postal_code`, `city`, `country_iso`). The user can change both addresses on the confirmation page, so read the shipping address from the order (or the `order.completed` webhook) before you ship. An item's `image_url` is copied to Anikeen ID and stays visible on the order and invoice: ```php $order = $user->orders()->create([ 'billing_address_id' => $addressId, 'shipping_address' => ['first_name' => 'Ada', 'last_name' => 'Lovelace', 'address' => 'Main St', 'house_number' => '1', 'postal_code' => '10115', 'city' => 'Berlin', 'country_iso' => 'DE'], 'items' => [ ['type' => 'physical', 'name' => 'T-shirt', 'unit' => 'piece', 'units' => 1, 'gross_price' => '24.90', 'image_url' => 'https://shop.test/img/shirt.png'], ['type' => 'physical-reduced', 'name' => 'Artbook', 'unit' => 'piece', 'units' => 1, 'gross_price' => '19.95'], ], ]); ``` Once a paid order is sent, create a shipment. Anikeen ID adds its delivery note to the user's order documents (with the tracking link) and sends `order.shipped` to your webhook endpoints. Without `items`, everything physical that is left is shipped; partial deliveries name the items: ```php $order = $user->orders()->find($orderId); $shipment = $order->shipments()->create([ 'carrier' => 'DHL', 'tracking_number' => '00340434161234567890', 'tracking_url' => 'https://www.dhl.de/…?piececode=00340434161234567890', // 'items' => [['order_item_id' => $itemId, 'units' => 1]], ]); $shipment->number; // LS-2026-0001 $shipment->deliveryNote(); // the delivery note as PDF ``` Only the app that created the order (or a trusted app) can ship it, and only once it is paid. `checkout()` on such a draft throws `ConfirmationRequiredException`; its `confirmationUrl` points to the same page. Trusted apps keep checking out directly. ### Webhooks Anikeen ID sends `order.*` and `subscription.*` events to the webhook endpoints of your app (set up in the Anikeen ID admin panel, which also shows the signing secret `whsec_…`). Verify each request with the raw body and answer 2xx within 10 seconds; failed deliveries are retried for about three days. ```php use Anikeen\Id\Exceptions\SignatureVerificationException; use Anikeen\Id\Webhooks\Webhook; Route::post('webhooks/anikeen', function (Request $request) { try { $event = Webhook::constructEvent($request->getContent(), $request->header('Anikeen-Signature'), config('services.anikeen.webhook_secret')); } catch (SignatureVerificationException) { abort(400); } match ($event['type']) { 'order.completed' => Shop::fulfil($event['data']['object']['id']), 'subscription.cancelled' => Panel::suspend($event['data']['object']['id']), default => null, }; return response()->noContent(); })->withoutMiddleware(VerifyCsrfToken::class); ``` Events can arrive more than once or out of order: use `$event['id']` to skip duplicates, and reload the object through the API when the current state matters. ### Change the default access token / refresh token field name If you access / refresh token fields differs from the default `anikeen_id_access_token` / `anikeen_id_refresh_token`, you can specify the field name in the `AppServiceProvider` boot method: ```php use Anikeen\Id\AnikeenId; public function boot(): void { AnikeenId::useAccessTokenField('anikeen_id_access_token'); AnikeenId::useRefreshTokenField('anikeen_id_refresh_token'); } ``` ### Implementing Auth This method should typically be called in the `boot` method of your `AppServiceProvider` class: ```php use Anikeen\Id\AnikeenId; use Anikeen\Id\Providers\AnikeenIdUserProvider; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; public function boot(): void { Auth::provider('anikeen', function ($app, array $config) { return new AnikeenIdUserProvider( $app->make(AnikeenId::class), $app->make(Request::class), $config['model'], $config['fields'] ?? [], ); }); } ``` reference the guard in the `guards` configuration of your `auth.php` configuration file: ```php 'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'users', ], 'api' => [ 'driver' => 'anikeen', 'provider' => 'anikeen', ], ], ``` reference the provider in the `providers` configuration of your `auth.php` configuration file: ```php 'providers' => [ 'users' => [ 'driver' => 'eloquent', 'model' => App\Models\User::class, ], 'anikeen' => [ 'driver' => 'anikeen', 'model' => App\Models\User::class, 'fields' => ['first_name', 'last_name', 'email'], ], ], ``` ## General #### Setters and Getters ```php $anikeenId = new Anikeen\Id\AnikeenId(); $anikeenId->setClientId('abc123'); $anikeenId->setClientSecret('abc456'); $anikeenId->setToken('abcdef123456'); $anikeenId = $anikeenId->withClientId('abc123'); $anikeenId = $anikeenId->withClientSecret('abc123'); $anikeenId = $anikeenId->withToken('abcdef123456'); ``` #### Error handling for an unsuccessful query: ```php $result = $anikeenId->sshKeysByUserId('someInvalidId'); // Check, if the query was successfully if (!$result->success()) { die('Ooops: ' . $result->error()); } ``` #### Shift result to get single key data: ```php $result = $anikeenId->sshKeysByUserId('someValidId'); $sshKey = $result->shift(); echo $sshKey->name; ``` ## Examples #### Get User SSH Key ```php $anikeenId = new Anikeen\IdAnikeenId(); $anikeenId->setClientId('abc123'); // Get SSH Key by User ID $result = $anikeenId->sshKeysByUserId('someValidId'); // Check, if the query was successfully if (!$result->success()) { die('Ooops: ' . $result->error()); } // Shift result to get single key data $sshKey = $result->shift(); echo $sshKey->name; ``` #### Create Order Preview ```php $anikeenId = new \Anikeen\Id\AnikeenId(); // Create new Order Preview $result = $anikeenId->createOrderPreview([ 'country_iso' => 'de', 'items' => [ [ 'type' => 'physical', 'name' => 'Test', 'price' => 2.99, 'unit' => 'onetime', 'units' => 1, ] ] ])->shift(); echo $preview->gross_total; ``` #### OAuth Tokens ```php $anikeenId = new Anikeen\Id\AnikeenId(); $anikeenId->setClientId('abc123'); $anikeenId->setToken('abcdef123456'); $result = $anikeenId->getAuthedUser(); $user = $userResult->shift(); ``` ```php $anikeenId->setToken('uvwxyz456789'); $result = $anikeenId->getAuthedUser(); ``` ```php $result = $anikeenId->withToken('uvwxyz456789')->getAuthedUser(); ``` #### Facade ```php use Anikeen\Id\Facades\AnikeenId; AnikeenId::withClientId('abc123')->withToken('abcdef123456')->getAuthedUser(); ``` ## Documentation ## AnikeenId ### Oauth ```php public function retrievingToken(string $grantType, array $attributes): Result ``` ### ManagesPricing ```php public function createOrderPreview(array $attributes = []): Result ``` ### ManagesProducts ```php public function products(): Result public function boxes(): Result ``` ### ManagesShippingLabels ```php public function shippingLabels(array $parameters = []): Result public function shippingLabel(string $id): Result public function shippingLabelPdf(string $id): ?string public function issueShippingLabel(array $attributes): Result public function shipShippingLabel(array $hex): Result public function shippingPriceList(): Result ``` ### ManagesUserBilling ```php public function appOrders(): AppOrders public function createOrderForUser(string $userId, array $attributes): Result public function createTransactionForUser(string $userId, array $attributes): Result ``` ### ManagesSocialiteUsers ```php public function socialiteUsers(): SocialiteUsers ``` ### ManagesSshKeys ```php public function sshKeysByUserId(string $userId): SshKeys ``` ### ManagesUsers ```php public function getAuthedUser(): Result public function createUser(array $attributes): Result public function findOrCreateSocialiteUser(string $provider, string $providerId, ?string $email = null, ?string $name = null): Result public function refreshToken(string $storedRefreshToken, string $scope = ''): Result public function isEmailExisting(string $email): Result public function setParent(mixed $parent): self public function getParent() ``` ## Billable ### ManagesAddresses ```php public function addresses(): Addresses public function hasDefaultBillingAddress(): bool public function setBillable(Billable|Model $billable): self public function getBillable(): Billable ``` ### ManagesBalance ```php public function balance(): float public function charges(): float public function billingOverview(): object public function charge(string|float $amount, string $paymentMethodId, array $options = []): Transaction public function setBillable(Billable|Model $billable): self public function getBillable(): Billable ``` ### ManagesCountries ```php public function countries(): Countries public function setBillable(Billable|Model $billable): self public function getBillable(): Billable ``` ### ManagesInvoices ```php public function invoices(array $parameters = []): Invoices public function setBillable(Billable|Model $billable): self public function getBillable(): Billable ``` ### ManagesOrders ```php public function orders(array $parameters = []): Orders public function setBillable(Billable|Model $billable): self public function getBillable(): Billable ``` ### ManagesPaymentMethods ```php public function paymentMethods(): PaymentMethods public function hasPaymentMethod(): ?PaymentMethod public function defaultPaymentMethod(): ?PaymentMethod public function hasDefaultPaymentMethod(): bool public function setDefaultPaymentMethod(string $id): PaymentMethod public function billingPortalUrl(?string $returnUrl = null, array $options = []): string public function createSetupIntent(array $options = []): Result public function setBillable(Billable|Model $billable): self public function getBillable(): Billable ``` ### ManagesProfile ```php public function profilePortalUrl(?string $returnUrl = null, array $options = []): string public function setBillable(Billable|Model $billable): self public function getBillable(): Billable ``` ### ManagesSubscriptions ```php public function subscriptions(): Subscriptions public function appSubscriptions(): AppSubscriptions public function appOrders(): AppOrders public function setBillable(Billable|Model $billable): self public function getBillable(): Billable ``` ### ManagesTaxation ```php public function vatRate(): float public function setBillable(Billable|Model $billable): self public function getBillable(): Billable ``` ### ManagesTransactions ```php public function transactions(): Transactions public function setBillable(Billable|Model $billable): self public function getBillable(): Billable ``` ### ManagesUserSshKeys ```php public function sshKeys(): SshKeys public function setBillable(Billable|Model $billable): self public function getBillable(): Billable ``` [**OAuth Scopes Enums**](https://github.com/anikeen-com/id/blob/main/src/Enums/Scope.php) ## Development #### Run Tests ```shell composer test ``` ```shell BASE_URL=xxxx CLIENT_ID=xxxx CLIENT_KEY=yyyy CLIENT_ACCESS_TOKEN=zzzz composer test ``` #### Generate Documentation ```shell composer docs ```