mirror of
https://github.com/anikeen-com/id.git
synced 2026-11-04 22:59:41 +00:00
Document payments end to end
Add a Payments guide that follows a typical third-party shop: sign-in scopes, user table and token storage, creating orders and subscriptions, the confirmation and return flow, statuses, prices and VAT, shipments, webhooks with Laravel 11 CSRF exclusion, error handling and testing. Move the trusted-app sections and API authentication after it, add the webhook secret to the config, fix the broken General/Examples snippets and the Scope enum link. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
26716e7bba
commit
e3e59fb8a4
2 files changed
+780
-366
No files matched your search
@@ -6,14 +6,21 @@
|
|||||||
|
|
||||||
PHP Anikeen ID API Client for Laravel 11+
|
PHP Anikeen ID API Client for Laravel 11+
|
||||||
|
|
||||||
|
Sign in with Anikeen ID and let your users pay with their Anikeen ID account: one-off orders,
|
||||||
|
subscriptions and physical goods, with invoices, VAT and payment handled by Anikeen ID.
|
||||||
|
|
||||||
## Table of contents
|
## Table of contents
|
||||||
|
|
||||||
1. [Installation](#installation)
|
1. [Installation](#installation)
|
||||||
2. [Configuration](#configuration)
|
2. [Configuration](#configuration)
|
||||||
3. [General](#general)
|
3. [Sign in with Anikeen ID](#sign-in-with-anikeen-id)
|
||||||
4. [Examples](#examples)
|
4. [Payments](#payments)
|
||||||
5. [Documentation](#documentation)
|
5. [Trusted apps](#trusted-apps)
|
||||||
6. [Development](#Development)
|
6. [API authentication](#api-authentication)
|
||||||
|
7. [General](#general)
|
||||||
|
8. [Examples](#examples)
|
||||||
|
9. [Documentation](#documentation)
|
||||||
|
10. [Development](#development)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -23,12 +30,20 @@ composer require anikeen/id
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Add environmental variables to your `.env` file:
|
Register your app in Anikeen ID first. You need:
|
||||||
|
|
||||||
|
- an OAuth client with your callback URL as redirect URI (e.g. `https://shop.test/auth/callback`),
|
||||||
|
- a webhook endpoint (e.g. `https://shop.test/webhooks/anikeen`) if you sell something; Anikeen ID
|
||||||
|
shows its signing secret `whsec_…`,
|
||||||
|
- optionally a logo, shown to your users on the login and confirmation pages.
|
||||||
|
|
||||||
|
Add the credentials to your `.env` file:
|
||||||
|
|
||||||
```
|
```
|
||||||
ANIKEEN_ID_KEY=
|
ANIKEEN_ID_KEY=
|
||||||
ANIKEEN_ID_SECRET=
|
ANIKEEN_ID_SECRET=
|
||||||
ANIKEEN_ID_CALLBACK_URL=http://localhost/auth/callback
|
ANIKEEN_ID_CALLBACK_URL=http://localhost/auth/callback
|
||||||
|
ANIKEEN_ID_WEBHOOK_SECRET=
|
||||||
```
|
```
|
||||||
|
|
||||||
To switch from `production` to `staging` use following variable:
|
To switch from `production` to `staging` use following variable:
|
||||||
@@ -48,9 +63,12 @@ Add to `config/services.php` file:
|
|||||||
'client_secret' => env('ANIKEEN_ID_SECRET'),
|
'client_secret' => env('ANIKEEN_ID_SECRET'),
|
||||||
'redirect' => env('ANIKEEN_ID_CALLBACK_URL'),
|
'redirect' => env('ANIKEEN_ID_CALLBACK_URL'),
|
||||||
'base_url' => env('ANIKEEN_ID_BASE_URL'),
|
'base_url' => env('ANIKEEN_ID_BASE_URL'),
|
||||||
|
'webhook_secret' => env('ANIKEEN_ID_WEBHOOK_SECRET'),
|
||||||
],
|
],
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Sign in with Anikeen ID
|
||||||
|
|
||||||
### Event Listener
|
### 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:
|
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:
|
||||||
@@ -64,21 +82,19 @@ public function boot(): void
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Registering Middleware
|
### User model
|
||||||
|
|
||||||
Append it to the global middleware stack in your application's `bootstrap/app.php` file:
|
Your users table needs the user's Anikeen ID and their tokens:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
->withMiddleware(function (Middleware $middleware) {
|
Schema::table('users', function (Blueprint $table) {
|
||||||
$middleware->web(append: [
|
$table->string('anikeen_id')->nullable()->unique();
|
||||||
\Anikeen\Id\Http\Middleware\CreateFreshApiToken::class,
|
$table->text('anikeen_id_access_token')->nullable();
|
||||||
]);
|
$table->text('anikeen_id_refresh_token')->nullable();
|
||||||
})
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### Implementing Billable
|
Add the `Billable` trait to the user model. Keep the tokens encrypted and out of JSON:
|
||||||
|
|
||||||
To implement the `Billable` trait, you need to add the `Billable` trait to your user model.
|
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Anikeen\Id\Billable;
|
use Anikeen\Id\Billable;
|
||||||
@@ -87,11 +103,336 @@ class User extends Authenticatable
|
|||||||
{
|
{
|
||||||
use Billable;
|
use Billable;
|
||||||
|
|
||||||
// Your model code...
|
protected $hidden = ['password', 'remember_token', 'anikeen_id_access_token', 'anikeen_id_refresh_token'];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'anikeen_id_access_token' => 'encrypted',
|
||||||
|
'anikeen_id_refresh_token' => 'encrypted',
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
then, you can use the `Billable` trait methods in your user model.
|
All user methods (`$user->orders()`, `$user->subscriptions()`, …) call the API with the user's
|
||||||
|
access token from `anikeen_id_access_token`.
|
||||||
|
|
||||||
|
### Login and callback
|
||||||
|
|
||||||
|
Ask for the scopes your app needs (see [Scope](src/Id/Enums/Scope.php)). To sell orders and
|
||||||
|
subscriptions, a shop typically asks for:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Laravel\Socialite\Facades\Socialite;
|
||||||
|
|
||||||
|
Route::get('auth/redirect', fn () => Socialite::driver('anikeen')
|
||||||
|
->scopes(['user:read', 'addresses:read', 'orders', 'subscriptions'])
|
||||||
|
->redirect());
|
||||||
|
|
||||||
|
Route::get('auth/callback', function () {
|
||||||
|
$account = Socialite::driver('anikeen')->user();
|
||||||
|
|
||||||
|
$user = User::updateOrCreate(['anikeen_id' => $account->getId()], [
|
||||||
|
'name' => $account->getName() ?: $account->getNickname(),
|
||||||
|
'email' => $account->getEmail(),
|
||||||
|
'anikeen_id_access_token' => $account->token,
|
||||||
|
'anikeen_id_refresh_token' => $account->refreshToken,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Auth::login($user, remember: true);
|
||||||
|
|
||||||
|
return redirect()->intended('/');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Access tokens expire. Get a new one with the refresh token (or send the user through the login
|
||||||
|
again):
|
||||||
|
|
||||||
|
```php
|
||||||
|
$result = app(\Anikeen\Id\AnikeenId::class)->refreshToken($user->anikeen_id_refresh_token);
|
||||||
|
|
||||||
|
if ($result->success()) {
|
||||||
|
$user->update([
|
||||||
|
'anikeen_id_access_token' => $result->data()->access_token,
|
||||||
|
'anikeen_id_refresh_token' => $result->data()->refresh_token,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Change the default access token / refresh token field name
|
||||||
|
|
||||||
|
If your access / refresh token fields differ 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');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Payments
|
||||||
|
|
||||||
|
### Which way fits?
|
||||||
|
|
||||||
|
| You want to sell | Use | Token | Who starts the payment |
|
||||||
|
|---|---|---|---|
|
||||||
|
| One-off purchases (digital or physical goods) | [Orders](#selling-an-order) | user | the user, on Anikeen ID |
|
||||||
|
| Something that recurs (a pass, a plan) | [Subscriptions](#subscriptions) | user | the user, on Anikeen ID |
|
||||||
|
| Several subscriptions in one step | [Checkout sessions](#checkout-sessions-the-user-confirms-several-subscriptions-at-once) | user or app | the user, on Anikeen ID |
|
||||||
|
| Usage billing from your backend, without the user online | [App subscriptions](#app-subscriptions-trusted-apps-no-user-token) | app | your app (trusted apps only) |
|
||||||
|
|
||||||
|
Most apps are third-party apps: they create an order or subscription as a draft, send the user to
|
||||||
|
Anikeen ID to confirm and pay, and get the user back afterwards. Anikeen ID shows the items, asks
|
||||||
|
for missing addresses, takes the payment and writes the invoice. Trusted apps (operated by
|
||||||
|
Anikeen) can also charge directly; see [Trusted apps](#trusted-apps).
|
||||||
|
|
||||||
|
The flow, as in a typical shop:
|
||||||
|
|
||||||
|
1. The user signs in with Anikeen ID ([above](#sign-in-with-anikeen-id)).
|
||||||
|
2. Your app creates the order: `$user->orders()->create([...])`.
|
||||||
|
3. Redirect the user to `$order->confirmationUrl($returnUrl)`.
|
||||||
|
4. The user confirms and pays on Anikeen ID, then comes back to `$returnUrl?status=…&order=…`.
|
||||||
|
5. Your app reads the order through the API (never trust the query string) and shows the result.
|
||||||
|
6. The `order.completed` webhook arrives; fulfil the order there.
|
||||||
|
|
||||||
|
### Selling an order
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Anikeen\Id\Exceptions\ResourceException;
|
||||||
|
|
||||||
|
$order = $user->orders()->create([
|
||||||
|
'billing_address_id' => $user->getUserData()->billing_address_id,
|
||||||
|
'items' => [
|
||||||
|
['type' => 'digital', 'name' => 'Sticker pack', 'unit' => 'piece', 'units' => 1, 'gross_price' => '9.99'],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$shopOrder = ShopOrder::create(['user_id' => $user->id, 'anikeen_id' => $order->id, 'status' => $order->status]); // your own order model
|
||||||
|
|
||||||
|
return redirect()->away($order->confirmationUrl(route('orders.return', $shopOrder)));
|
||||||
|
// later: https://shop.test/orders/1/return?status=completed&order=01J…
|
||||||
|
```
|
||||||
|
|
||||||
|
When the user has no billing address on Anikeen ID yet, pass one instead of the ID. A name and
|
||||||
|
the country are enough; the user completes it on the confirmation page:
|
||||||
|
|
||||||
|
```php
|
||||||
|
'billing_address' => ['first_name' => 'Ada', 'last_name' => 'Lovelace', 'country_iso' => 'DE', 'email' => $user->email],
|
||||||
|
```
|
||||||
|
|
||||||
|
Back in your app, read the order to know its state:
|
||||||
|
|
||||||
|
```php
|
||||||
|
Route::get('orders/{shopOrder}/return', function (ShopOrder $shopOrder) {
|
||||||
|
$order = auth()->user()->orders()->find($shopOrder->anikeen_id);
|
||||||
|
|
||||||
|
$shopOrder->update(['status' => $order->status]);
|
||||||
|
|
||||||
|
return redirect()->route('orders.show', $shopOrder);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The order's `status`:
|
||||||
|
|
||||||
|
| `status` | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `draft` | Created, not confirmed by the user yet |
|
||||||
|
| `pending` | Confirmed, the payment is still processing |
|
||||||
|
| `completed` | Paid |
|
||||||
|
| `cancelled` | The user cancelled, or it wasn't confirmed within 24 hours |
|
||||||
|
|
||||||
|
`return_url` must have the origin of one of your app's redirect URIs. The `status` in its query
|
||||||
|
string (`completed`, `pending` or `cancelled`) is only a hint for the message you show.
|
||||||
|
|
||||||
|
`checkout()` on such a draft throws `ConfirmationRequiredException`; its `confirmationUrl`
|
||||||
|
points to the same page. Trusted apps keep checking out directly.
|
||||||
|
|
||||||
|
### Prices and VAT
|
||||||
|
|
||||||
|
Anikeen ID calculates VAT from the user's billing address and the item `type`:
|
||||||
|
|
||||||
|
| `type` | For | VAT in Germany |
|
||||||
|
|---|---|---|
|
||||||
|
| `digital` | Downloads, streams, services | 19 % |
|
||||||
|
| `physical` | Goods you ship | 19 % |
|
||||||
|
| `physical-reduced` | Goods with the reduced rate (books, event tickets, …) | 7 % |
|
||||||
|
|
||||||
|
Give each item either `gross_price` (VAT included, what the user pays) or `price` (net), per unit.
|
||||||
|
`unit` is `piece` or `onetime` for orders; `units` is the quantity. Subscription prices are always
|
||||||
|
net per unit (`hour`, `day`, `week`, `month`, `year`).
|
||||||
|
|
||||||
|
To show a total before the user confirms, ask for a preview:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$preview = app(\Anikeen\Id\AnikeenId::class)->createOrderPreview([
|
||||||
|
'country_iso' => 'DE',
|
||||||
|
'items' => [['type' => 'physical', 'name' => 'T-shirt', 'gross_price' => '24.90', 'unit' => 'piece', 'units' => 1]],
|
||||||
|
])->shift();
|
||||||
|
|
||||||
|
$preview->gross_total;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Physical goods and shipments
|
||||||
|
|
||||||
|
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), mails it to the user (`'notify_customer' => false` if
|
||||||
|
your shop sends its own mail) 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
|
||||||
|
```
|
||||||
|
|
||||||
|
To know what is left to ship, compare the order's items with its shipments:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$items = $order->orderItems()->toArray(); // id, type, name, units, …
|
||||||
|
$shipments = $order->shipments()->toArray(); // each with items: [{order_item_id, units}]
|
||||||
|
```
|
||||||
|
|
||||||
|
Only the app that created the order (or a trusted app) can ship it, and only once it is paid.
|
||||||
|
|
||||||
|
### Subscriptions
|
||||||
|
|
||||||
|
Subscriptions work like orders: create a draft, let the user confirm it, read it back.
|
||||||
|
|
||||||
|
```php
|
||||||
|
$subscription = $user->subscriptions()->create([
|
||||||
|
'name' => 'Pixel Stream Pass',
|
||||||
|
'description' => 'All streams, every month',
|
||||||
|
'unit' => 'month',
|
||||||
|
'price' => 6.714286, // net per unit, 7.99 with 19 % VAT
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->away($subscription->confirmationUrl(route('pass.return')));
|
||||||
|
// later: https://shop.test/pass/return?status=completed&subscription=01J…
|
||||||
|
```
|
||||||
|
|
||||||
|
A subscription has `active` (running and paid), `cancelled` and `autorenew` (false once paused).
|
||||||
|
After the return, check `$user->subscriptions()->find($id)->active`.
|
||||||
|
|
||||||
|
### Webhooks
|
||||||
|
|
||||||
|
Anikeen ID sends `order.*`, `subscription.*` and `checkout.*` events to the webhook endpoints of
|
||||||
|
your app (see [`EventType`](src/Id/Webhooks/EventType.php) for all of them). Verify each request
|
||||||
|
with the raw body and answer 2xx within 10 seconds; failed deliveries are retried for about three
|
||||||
|
days.
|
||||||
|
|
||||||
|
Exclude the route from CSRF protection in `bootstrap/app.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
->withMiddleware(function (Middleware $middleware) {
|
||||||
|
$middleware->validateCsrfTokens(except: ['webhooks/*']);
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Anikeen\Id\Exceptions\SignatureVerificationException;
|
||||||
|
use Anikeen\Id\Webhooks\EventType;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! ProcessedEvent::firstOrCreate(['id' => $event['id']])->wasRecentlyCreated) {
|
||||||
|
return response()->noContent(); // already handled
|
||||||
|
}
|
||||||
|
|
||||||
|
$object = $event['data']['object'];
|
||||||
|
|
||||||
|
match ($event['type']) {
|
||||||
|
EventType::ORDER_COMPLETED => Shop::fulfil($object['id'], $object['shipping_address']),
|
||||||
|
EventType::ORDER_CANCELLED => Shop::release($object['id']),
|
||||||
|
EventType::SUBSCRIPTION_ACTIVATED => Pass::grant($object['id']),
|
||||||
|
EventType::SUBSCRIPTION_CANCELLED => Pass::revoke($object['id']),
|
||||||
|
EventType::CHECKOUT_COMPLETED => Panel::provision($object['subscription_ids']),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return response()->noContent();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
`data.object` is the order, subscription or checkout session as the API returns it. 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. The admin panel can send a
|
||||||
|
`webhook.test` event to check your endpoint.
|
||||||
|
|
||||||
|
### Errors
|
||||||
|
|
||||||
|
Calls with the user's token throw `Anikeen\Id\Exceptions\ResourceException` when Anikeen ID
|
||||||
|
rejects them; `getCode()` is the HTTP status (e.g. `422` for invalid data, `404` for an unknown
|
||||||
|
ID) and `getMessage()` the reason. App calls (`app*()`) throw its subclass `ApiException`, which
|
||||||
|
adds a stable `error` code. Catching `ResourceException` covers both:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Anikeen\Id\Exceptions\ResourceException;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$order = $user->orders()->create($attributes);
|
||||||
|
} catch (ResourceException $e) {
|
||||||
|
report($e);
|
||||||
|
|
||||||
|
return back()->with('error', 'The order could not be created.');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
Develop against staging (`ANIKEEN_ID_MODE=staging`) with an OAuth client registered there.
|
||||||
|
Point `ANIKEEN_ID_BASE_URL` at your own Anikeen ID instance if you run one locally.
|
||||||
|
|
||||||
|
In unit tests, swap the HTTP client for a Guzzle `MockHandler`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$mock = new \GuzzleHttp\Handler\MockHandler([
|
||||||
|
new \GuzzleHttp\Psr7\Response(201, ['Content-Type' => 'application/json'], json_encode(['data' => ['id' => '01JORDER', 'status' => 'draft']])),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->app->instance(AnikeenId::class, (new AnikeenId())->setHttpClient(new \GuzzleHttp\Client([
|
||||||
|
'handler' => \GuzzleHttp\HandlerStack::create($mock),
|
||||||
|
'base_uri' => 'https://id.test/api/',
|
||||||
|
])));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Trusted apps
|
||||||
|
|
||||||
|
Apps operated by Anikeen can be marked as trusted. They can charge without asking the user and
|
||||||
|
use their own token (client credentials), so billing keeps working when the user's login expired.
|
||||||
|
|
||||||
### App subscriptions (trusted apps, no user token)
|
### App subscriptions (trusted apps, no user token)
|
||||||
|
|
||||||
@@ -166,7 +507,7 @@ period, confirms (all subscriptions start, or none) or cancels, and returns to `
|
|||||||
Open sessions expire after 24 hours with their drafts. Trusted apps can still check out directly;
|
Open sessions expire after 24 hours with their drafts. Trusted apps can still check out directly;
|
||||||
sessions are opt-in.
|
sessions are opt-in.
|
||||||
|
|
||||||
Example: the Console's builder ("Jetzt kaufen") with several products and coupon prices:
|
Example: a cart with several products and coupon prices:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Anikeen\Id\Exceptions\ApiException;
|
use Anikeen\Id\Exceptions\ApiException;
|
||||||
@@ -214,113 +555,22 @@ While a session is open, its subscriptions can't be checked out or repriced by t
|
|||||||
`subscription.activated` / `subscription.cancelled` are still sent per subscription, before the
|
`subscription.activated` / `subscription.cancelled` are still sent per subscription, before the
|
||||||
`checkout.*` event of the session.
|
`checkout.*` event of the session.
|
||||||
|
|
||||||
### 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),
|
## API authentication
|
||||||
but can't charge them. The user confirms on Anikeen ID and comes back to your `return_url`:
|
|
||||||
|
Use this when your own API accepts access tokens issued by Anikeen ID.
|
||||||
|
|
||||||
|
### Registering Middleware
|
||||||
|
|
||||||
|
To call your API from your own frontend with the user's session, append the middleware to the
|
||||||
|
web middleware group in your application's `bootstrap/app.php` file:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Anikeen\Id\Exceptions\ConfirmationRequiredException;
|
->withMiddleware(function (Middleware $middleware) {
|
||||||
|
$middleware->web(append: [
|
||||||
$order = $user->orders()->create([
|
\Anikeen\Id\Http\Middleware\CreateFreshApiToken::class,
|
||||||
'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), mails it to the user (`'notify_customer' => false` if
|
|
||||||
your shop sends its own mail) 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.*`, `subscription.*` and `checkout.*` 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\EventType;
|
|
||||||
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']) {
|
|
||||||
EventType::ORDER_COMPLETED => Shop::fulfil($event['data']['object']['id']),
|
|
||||||
EventType::SUBSCRIPTION_CANCELLED => Panel::suspend($event['data']['object']['id']),
|
|
||||||
EventType::CHECKOUT_COMPLETED => Panel::provision($event['data']['object']['subscription_ids']),
|
|
||||||
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 (API tokens)
|
### Implementing Auth (API tokens)
|
||||||
@@ -408,70 +658,36 @@ $anikeenId = $anikeenId->withClientSecret('abc123');
|
|||||||
$anikeenId = $anikeenId->withToken('abcdef123456');
|
$anikeenId = $anikeenId->withToken('abcdef123456');
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Error handling for an unsuccessful query:
|
#### Results and resources
|
||||||
|
|
||||||
|
Low-level methods on `AnikeenId` (e.g. `getAuthedUser()`, `createOrderPreview()`,
|
||||||
|
`refreshToken()`) return a `Result`. Check it before you use it:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$result = $anikeenId->sshKeysByUserId('someInvalidId');
|
$result = $anikeenId->getAuthedUser();
|
||||||
|
|
||||||
// Check, if the query was successfully
|
|
||||||
if (!$result->success()) {
|
if (!$result->success()) {
|
||||||
die('Ooops: ' . $result->error());
|
die('Ooops: ' . $result->error());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$user = $result->shift(); // the object, or the first item of a list
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Shift result to get single key data:
|
Resource methods (`$user->orders()`, `$anikeenId->sshKeysByUserId()`, …) return objects and throw
|
||||||
|
`ResourceException` instead (see [Errors](#errors)).
|
||||||
```php
|
|
||||||
$result = $anikeenId->sshKeysByUserId('someValidId');
|
|
||||||
|
|
||||||
$sshKey = $result->shift();
|
|
||||||
|
|
||||||
echo $sshKey->name;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
#### Get User SSH Key
|
#### Get User SSH Keys
|
||||||
|
|
||||||
|
Uses the app's own token (client credentials):
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$anikeenId = new Anikeen\IdAnikeenId();
|
$sshKeys = app(Anikeen\Id\AnikeenId::class)->sshKeysByUserId($anikeenUserId);
|
||||||
|
|
||||||
$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();
|
|
||||||
|
|
||||||
|
foreach ($sshKeys->toArray() as $sshKey) {
|
||||||
echo $sshKey->name;
|
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
|
#### OAuth Tokens
|
||||||
@@ -479,22 +695,13 @@ echo $preview->gross_total;
|
|||||||
```php
|
```php
|
||||||
$anikeenId = new Anikeen\Id\AnikeenId();
|
$anikeenId = new Anikeen\Id\AnikeenId();
|
||||||
|
|
||||||
$anikeenId->setClientId('abc123');
|
|
||||||
$anikeenId->setToken('abcdef123456');
|
$anikeenId->setToken('abcdef123456');
|
||||||
|
|
||||||
$result = $anikeenId->getAuthedUser();
|
$user = $anikeenId->getAuthedUser()->shift();
|
||||||
|
|
||||||
$user = $userResult->shift();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$anikeenId->setToken('uvwxyz456789');
|
$user = $anikeenId->withToken('uvwxyz456789')->getAuthedUser()->shift();
|
||||||
|
|
||||||
$result = $anikeenId->getAuthedUser();
|
|
||||||
```
|
|
||||||
|
|
||||||
```php
|
|
||||||
$result = $anikeenId->withToken('uvwxyz456789')->getAuthedUser();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Facade
|
#### Facade
|
||||||
@@ -502,7 +709,7 @@ $result = $anikeenId->withToken('uvwxyz456789')->getAuthedUser();
|
|||||||
```php
|
```php
|
||||||
use Anikeen\Id\Facades\AnikeenId;
|
use Anikeen\Id\Facades\AnikeenId;
|
||||||
|
|
||||||
AnikeenId::withClientId('abc123')->withToken('abcdef123456')->getAuthedUser();
|
AnikeenId::withToken('abcdef123456')->getAuthedUser();
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
@@ -678,7 +885,7 @@ public function getBillable(): Billable
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
[**OAuth Scopes Enums**](https://github.com/anikeen-com/id/blob/main/src/Enums/Scope.php)
|
[**OAuth Scopes Enums**](https://github.com/anikeen-com/id/blob/main/src/Id/Enums/Scope.php)
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
+390
-183
@@ -6,14 +6,21 @@
|
|||||||
|
|
||||||
PHP Anikeen ID API Client for Laravel 11+
|
PHP Anikeen ID API Client for Laravel 11+
|
||||||
|
|
||||||
|
Sign in with Anikeen ID and let your users pay with their Anikeen ID account: one-off orders,
|
||||||
|
subscriptions and physical goods, with invoices, VAT and payment handled by Anikeen ID.
|
||||||
|
|
||||||
## Table of contents
|
## Table of contents
|
||||||
|
|
||||||
1. [Installation](#installation)
|
1. [Installation](#installation)
|
||||||
2. [Configuration](#configuration)
|
2. [Configuration](#configuration)
|
||||||
3. [General](#general)
|
3. [Sign in with Anikeen ID](#sign-in-with-anikeen-id)
|
||||||
4. [Examples](#examples)
|
4. [Payments](#payments)
|
||||||
5. [Documentation](#documentation)
|
5. [Trusted apps](#trusted-apps)
|
||||||
6. [Development](#Development)
|
6. [API authentication](#api-authentication)
|
||||||
|
7. [General](#general)
|
||||||
|
8. [Examples](#examples)
|
||||||
|
9. [Documentation](#documentation)
|
||||||
|
10. [Development](#development)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -23,12 +30,20 @@ composer require anikeen/id
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Add environmental variables to your `.env` file:
|
Register your app in Anikeen ID first. You need:
|
||||||
|
|
||||||
|
- an OAuth client with your callback URL as redirect URI (e.g. `https://shop.test/auth/callback`),
|
||||||
|
- a webhook endpoint (e.g. `https://shop.test/webhooks/anikeen`) if you sell something; Anikeen ID
|
||||||
|
shows its signing secret `whsec_…`,
|
||||||
|
- optionally a logo, shown to your users on the login and confirmation pages.
|
||||||
|
|
||||||
|
Add the credentials to your `.env` file:
|
||||||
|
|
||||||
```
|
```
|
||||||
ANIKEEN_ID_KEY=
|
ANIKEEN_ID_KEY=
|
||||||
ANIKEEN_ID_SECRET=
|
ANIKEEN_ID_SECRET=
|
||||||
ANIKEEN_ID_CALLBACK_URL=http://localhost/auth/callback
|
ANIKEEN_ID_CALLBACK_URL=http://localhost/auth/callback
|
||||||
|
ANIKEEN_ID_WEBHOOK_SECRET=
|
||||||
```
|
```
|
||||||
|
|
||||||
To switch from `production` to `staging` use following variable:
|
To switch from `production` to `staging` use following variable:
|
||||||
@@ -48,9 +63,12 @@ Add to `config/services.php` file:
|
|||||||
'client_secret' => env('ANIKEEN_ID_SECRET'),
|
'client_secret' => env('ANIKEEN_ID_SECRET'),
|
||||||
'redirect' => env('ANIKEEN_ID_CALLBACK_URL'),
|
'redirect' => env('ANIKEEN_ID_CALLBACK_URL'),
|
||||||
'base_url' => env('ANIKEEN_ID_BASE_URL'),
|
'base_url' => env('ANIKEEN_ID_BASE_URL'),
|
||||||
|
'webhook_secret' => env('ANIKEEN_ID_WEBHOOK_SECRET'),
|
||||||
],
|
],
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Sign in with Anikeen ID
|
||||||
|
|
||||||
### Event Listener
|
### 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:
|
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:
|
||||||
@@ -64,21 +82,19 @@ public function boot(): void
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Registering Middleware
|
### User model
|
||||||
|
|
||||||
Append it to the global middleware stack in your application's `bootstrap/app.php` file:
|
Your users table needs the user's Anikeen ID and their tokens:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
->withMiddleware(function (Middleware $middleware) {
|
Schema::table('users', function (Blueprint $table) {
|
||||||
$middleware->web(append: [
|
$table->string('anikeen_id')->nullable()->unique();
|
||||||
\Anikeen\Id\Http\Middleware\CreateFreshApiToken::class,
|
$table->text('anikeen_id_access_token')->nullable();
|
||||||
]);
|
$table->text('anikeen_id_refresh_token')->nullable();
|
||||||
})
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### Implementing Billable
|
Add the `Billable` trait to the user model. Keep the tokens encrypted and out of JSON:
|
||||||
|
|
||||||
To implement the `Billable` trait, you need to add the `Billable` trait to your user model.
|
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Anikeen\Id\Billable;
|
use Anikeen\Id\Billable;
|
||||||
@@ -87,11 +103,336 @@ class User extends Authenticatable
|
|||||||
{
|
{
|
||||||
use Billable;
|
use Billable;
|
||||||
|
|
||||||
// Your model code...
|
protected $hidden = ['password', 'remember_token', 'anikeen_id_access_token', 'anikeen_id_refresh_token'];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'anikeen_id_access_token' => 'encrypted',
|
||||||
|
'anikeen_id_refresh_token' => 'encrypted',
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
then, you can use the `Billable` trait methods in your user model.
|
All user methods (`$user->orders()`, `$user->subscriptions()`, …) call the API with the user's
|
||||||
|
access token from `anikeen_id_access_token`.
|
||||||
|
|
||||||
|
### Login and callback
|
||||||
|
|
||||||
|
Ask for the scopes your app needs (see [Scope](src/Id/Enums/Scope.php)). To sell orders and
|
||||||
|
subscriptions, a shop typically asks for:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Laravel\Socialite\Facades\Socialite;
|
||||||
|
|
||||||
|
Route::get('auth/redirect', fn () => Socialite::driver('anikeen')
|
||||||
|
->scopes(['user:read', 'addresses:read', 'orders', 'subscriptions'])
|
||||||
|
->redirect());
|
||||||
|
|
||||||
|
Route::get('auth/callback', function () {
|
||||||
|
$account = Socialite::driver('anikeen')->user();
|
||||||
|
|
||||||
|
$user = User::updateOrCreate(['anikeen_id' => $account->getId()], [
|
||||||
|
'name' => $account->getName() ?: $account->getNickname(),
|
||||||
|
'email' => $account->getEmail(),
|
||||||
|
'anikeen_id_access_token' => $account->token,
|
||||||
|
'anikeen_id_refresh_token' => $account->refreshToken,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Auth::login($user, remember: true);
|
||||||
|
|
||||||
|
return redirect()->intended('/');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Access tokens expire. Get a new one with the refresh token (or send the user through the login
|
||||||
|
again):
|
||||||
|
|
||||||
|
```php
|
||||||
|
$result = app(\Anikeen\Id\AnikeenId::class)->refreshToken($user->anikeen_id_refresh_token);
|
||||||
|
|
||||||
|
if ($result->success()) {
|
||||||
|
$user->update([
|
||||||
|
'anikeen_id_access_token' => $result->data()->access_token,
|
||||||
|
'anikeen_id_refresh_token' => $result->data()->refresh_token,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Change the default access token / refresh token field name
|
||||||
|
|
||||||
|
If your access / refresh token fields differ 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');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Payments
|
||||||
|
|
||||||
|
### Which way fits?
|
||||||
|
|
||||||
|
| You want to sell | Use | Token | Who starts the payment |
|
||||||
|
|---|---|---|---|
|
||||||
|
| One-off purchases (digital or physical goods) | [Orders](#selling-an-order) | user | the user, on Anikeen ID |
|
||||||
|
| Something that recurs (a pass, a plan) | [Subscriptions](#subscriptions) | user | the user, on Anikeen ID |
|
||||||
|
| Several subscriptions in one step | [Checkout sessions](#checkout-sessions-the-user-confirms-several-subscriptions-at-once) | user or app | the user, on Anikeen ID |
|
||||||
|
| Usage billing from your backend, without the user online | [App subscriptions](#app-subscriptions-trusted-apps-no-user-token) | app | your app (trusted apps only) |
|
||||||
|
|
||||||
|
Most apps are third-party apps: they create an order or subscription as a draft, send the user to
|
||||||
|
Anikeen ID to confirm and pay, and get the user back afterwards. Anikeen ID shows the items, asks
|
||||||
|
for missing addresses, takes the payment and writes the invoice. Trusted apps (operated by
|
||||||
|
Anikeen) can also charge directly; see [Trusted apps](#trusted-apps).
|
||||||
|
|
||||||
|
The flow, as in a typical shop:
|
||||||
|
|
||||||
|
1. The user signs in with Anikeen ID ([above](#sign-in-with-anikeen-id)).
|
||||||
|
2. Your app creates the order: `$user->orders()->create([...])`.
|
||||||
|
3. Redirect the user to `$order->confirmationUrl($returnUrl)`.
|
||||||
|
4. The user confirms and pays on Anikeen ID, then comes back to `$returnUrl?status=…&order=…`.
|
||||||
|
5. Your app reads the order through the API (never trust the query string) and shows the result.
|
||||||
|
6. The `order.completed` webhook arrives; fulfil the order there.
|
||||||
|
|
||||||
|
### Selling an order
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Anikeen\Id\Exceptions\ResourceException;
|
||||||
|
|
||||||
|
$order = $user->orders()->create([
|
||||||
|
'billing_address_id' => $user->getUserData()->billing_address_id,
|
||||||
|
'items' => [
|
||||||
|
['type' => 'digital', 'name' => 'Sticker pack', 'unit' => 'piece', 'units' => 1, 'gross_price' => '9.99'],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$shopOrder = ShopOrder::create(['user_id' => $user->id, 'anikeen_id' => $order->id, 'status' => $order->status]); // your own order model
|
||||||
|
|
||||||
|
return redirect()->away($order->confirmationUrl(route('orders.return', $shopOrder)));
|
||||||
|
// later: https://shop.test/orders/1/return?status=completed&order=01J…
|
||||||
|
```
|
||||||
|
|
||||||
|
When the user has no billing address on Anikeen ID yet, pass one instead of the ID. A name and
|
||||||
|
the country are enough; the user completes it on the confirmation page:
|
||||||
|
|
||||||
|
```php
|
||||||
|
'billing_address' => ['first_name' => 'Ada', 'last_name' => 'Lovelace', 'country_iso' => 'DE', 'email' => $user->email],
|
||||||
|
```
|
||||||
|
|
||||||
|
Back in your app, read the order to know its state:
|
||||||
|
|
||||||
|
```php
|
||||||
|
Route::get('orders/{shopOrder}/return', function (ShopOrder $shopOrder) {
|
||||||
|
$order = auth()->user()->orders()->find($shopOrder->anikeen_id);
|
||||||
|
|
||||||
|
$shopOrder->update(['status' => $order->status]);
|
||||||
|
|
||||||
|
return redirect()->route('orders.show', $shopOrder);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The order's `status`:
|
||||||
|
|
||||||
|
| `status` | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `draft` | Created, not confirmed by the user yet |
|
||||||
|
| `pending` | Confirmed, the payment is still processing |
|
||||||
|
| `completed` | Paid |
|
||||||
|
| `cancelled` | The user cancelled, or it wasn't confirmed within 24 hours |
|
||||||
|
|
||||||
|
`return_url` must have the origin of one of your app's redirect URIs. The `status` in its query
|
||||||
|
string (`completed`, `pending` or `cancelled`) is only a hint for the message you show.
|
||||||
|
|
||||||
|
`checkout()` on such a draft throws `ConfirmationRequiredException`; its `confirmationUrl`
|
||||||
|
points to the same page. Trusted apps keep checking out directly.
|
||||||
|
|
||||||
|
### Prices and VAT
|
||||||
|
|
||||||
|
Anikeen ID calculates VAT from the user's billing address and the item `type`:
|
||||||
|
|
||||||
|
| `type` | For | VAT in Germany |
|
||||||
|
|---|---|---|
|
||||||
|
| `digital` | Downloads, streams, services | 19 % |
|
||||||
|
| `physical` | Goods you ship | 19 % |
|
||||||
|
| `physical-reduced` | Goods with the reduced rate (books, event tickets, …) | 7 % |
|
||||||
|
|
||||||
|
Give each item either `gross_price` (VAT included, what the user pays) or `price` (net), per unit.
|
||||||
|
`unit` is `piece` or `onetime` for orders; `units` is the quantity. Subscription prices are always
|
||||||
|
net per unit (`hour`, `day`, `week`, `month`, `year`).
|
||||||
|
|
||||||
|
To show a total before the user confirms, ask for a preview:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$preview = app(\Anikeen\Id\AnikeenId::class)->createOrderPreview([
|
||||||
|
'country_iso' => 'DE',
|
||||||
|
'items' => [['type' => 'physical', 'name' => 'T-shirt', 'gross_price' => '24.90', 'unit' => 'piece', 'units' => 1]],
|
||||||
|
])->shift();
|
||||||
|
|
||||||
|
$preview->gross_total;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Physical goods and shipments
|
||||||
|
|
||||||
|
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), mails it to the user (`'notify_customer' => false` if
|
||||||
|
your shop sends its own mail) 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
|
||||||
|
```
|
||||||
|
|
||||||
|
To know what is left to ship, compare the order's items with its shipments:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$items = $order->orderItems()->toArray(); // id, type, name, units, …
|
||||||
|
$shipments = $order->shipments()->toArray(); // each with items: [{order_item_id, units}]
|
||||||
|
```
|
||||||
|
|
||||||
|
Only the app that created the order (or a trusted app) can ship it, and only once it is paid.
|
||||||
|
|
||||||
|
### Subscriptions
|
||||||
|
|
||||||
|
Subscriptions work like orders: create a draft, let the user confirm it, read it back.
|
||||||
|
|
||||||
|
```php
|
||||||
|
$subscription = $user->subscriptions()->create([
|
||||||
|
'name' => 'Pixel Stream Pass',
|
||||||
|
'description' => 'All streams, every month',
|
||||||
|
'unit' => 'month',
|
||||||
|
'price' => 6.714286, // net per unit, 7.99 with 19 % VAT
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->away($subscription->confirmationUrl(route('pass.return')));
|
||||||
|
// later: https://shop.test/pass/return?status=completed&subscription=01J…
|
||||||
|
```
|
||||||
|
|
||||||
|
A subscription has `active` (running and paid), `cancelled` and `autorenew` (false once paused).
|
||||||
|
After the return, check `$user->subscriptions()->find($id)->active`.
|
||||||
|
|
||||||
|
### Webhooks
|
||||||
|
|
||||||
|
Anikeen ID sends `order.*`, `subscription.*` and `checkout.*` events to the webhook endpoints of
|
||||||
|
your app (see [`EventType`](src/Id/Webhooks/EventType.php) for all of them). Verify each request
|
||||||
|
with the raw body and answer 2xx within 10 seconds; failed deliveries are retried for about three
|
||||||
|
days.
|
||||||
|
|
||||||
|
Exclude the route from CSRF protection in `bootstrap/app.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
->withMiddleware(function (Middleware $middleware) {
|
||||||
|
$middleware->validateCsrfTokens(except: ['webhooks/*']);
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Anikeen\Id\Exceptions\SignatureVerificationException;
|
||||||
|
use Anikeen\Id\Webhooks\EventType;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! ProcessedEvent::firstOrCreate(['id' => $event['id']])->wasRecentlyCreated) {
|
||||||
|
return response()->noContent(); // already handled
|
||||||
|
}
|
||||||
|
|
||||||
|
$object = $event['data']['object'];
|
||||||
|
|
||||||
|
match ($event['type']) {
|
||||||
|
EventType::ORDER_COMPLETED => Shop::fulfil($object['id'], $object['shipping_address']),
|
||||||
|
EventType::ORDER_CANCELLED => Shop::release($object['id']),
|
||||||
|
EventType::SUBSCRIPTION_ACTIVATED => Pass::grant($object['id']),
|
||||||
|
EventType::SUBSCRIPTION_CANCELLED => Pass::revoke($object['id']),
|
||||||
|
EventType::CHECKOUT_COMPLETED => Panel::provision($object['subscription_ids']),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return response()->noContent();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
`data.object` is the order, subscription or checkout session as the API returns it. 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. The admin panel can send a
|
||||||
|
`webhook.test` event to check your endpoint.
|
||||||
|
|
||||||
|
### Errors
|
||||||
|
|
||||||
|
Calls with the user's token throw `Anikeen\Id\Exceptions\ResourceException` when Anikeen ID
|
||||||
|
rejects them; `getCode()` is the HTTP status (e.g. `422` for invalid data, `404` for an unknown
|
||||||
|
ID) and `getMessage()` the reason. App calls (`app*()`) throw its subclass `ApiException`, which
|
||||||
|
adds a stable `error` code. Catching `ResourceException` covers both:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Anikeen\Id\Exceptions\ResourceException;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$order = $user->orders()->create($attributes);
|
||||||
|
} catch (ResourceException $e) {
|
||||||
|
report($e);
|
||||||
|
|
||||||
|
return back()->with('error', 'The order could not be created.');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
Develop against staging (`ANIKEEN_ID_MODE=staging`) with an OAuth client registered there.
|
||||||
|
Point `ANIKEEN_ID_BASE_URL` at your own Anikeen ID instance if you run one locally.
|
||||||
|
|
||||||
|
In unit tests, swap the HTTP client for a Guzzle `MockHandler`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$mock = new \GuzzleHttp\Handler\MockHandler([
|
||||||
|
new \GuzzleHttp\Psr7\Response(201, ['Content-Type' => 'application/json'], json_encode(['data' => ['id' => '01JORDER', 'status' => 'draft']])),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->app->instance(AnikeenId::class, (new AnikeenId())->setHttpClient(new \GuzzleHttp\Client([
|
||||||
|
'handler' => \GuzzleHttp\HandlerStack::create($mock),
|
||||||
|
'base_uri' => 'https://id.test/api/',
|
||||||
|
])));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Trusted apps
|
||||||
|
|
||||||
|
Apps operated by Anikeen can be marked as trusted. They can charge without asking the user and
|
||||||
|
use their own token (client credentials), so billing keeps working when the user's login expired.
|
||||||
|
|
||||||
### App subscriptions (trusted apps, no user token)
|
### App subscriptions (trusted apps, no user token)
|
||||||
|
|
||||||
@@ -166,7 +507,7 @@ period, confirms (all subscriptions start, or none) or cancels, and returns to `
|
|||||||
Open sessions expire after 24 hours with their drafts. Trusted apps can still check out directly;
|
Open sessions expire after 24 hours with their drafts. Trusted apps can still check out directly;
|
||||||
sessions are opt-in.
|
sessions are opt-in.
|
||||||
|
|
||||||
Example: the Console's builder ("Jetzt kaufen") with several products and coupon prices:
|
Example: a cart with several products and coupon prices:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Anikeen\Id\Exceptions\ApiException;
|
use Anikeen\Id\Exceptions\ApiException;
|
||||||
@@ -214,113 +555,22 @@ While a session is open, its subscriptions can't be checked out or repriced by t
|
|||||||
`subscription.activated` / `subscription.cancelled` are still sent per subscription, before the
|
`subscription.activated` / `subscription.cancelled` are still sent per subscription, before the
|
||||||
`checkout.*` event of the session.
|
`checkout.*` event of the session.
|
||||||
|
|
||||||
### 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),
|
## API authentication
|
||||||
but can't charge them. The user confirms on Anikeen ID and comes back to your `return_url`:
|
|
||||||
|
Use this when your own API accepts access tokens issued by Anikeen ID.
|
||||||
|
|
||||||
|
### Registering Middleware
|
||||||
|
|
||||||
|
To call your API from your own frontend with the user's session, append the middleware to the
|
||||||
|
web middleware group in your application's `bootstrap/app.php` file:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Anikeen\Id\Exceptions\ConfirmationRequiredException;
|
->withMiddleware(function (Middleware $middleware) {
|
||||||
|
$middleware->web(append: [
|
||||||
$order = $user->orders()->create([
|
\Anikeen\Id\Http\Middleware\CreateFreshApiToken::class,
|
||||||
'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), mails it to the user (`'notify_customer' => false` if
|
|
||||||
your shop sends its own mail) 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.*`, `subscription.*` and `checkout.*` 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\EventType;
|
|
||||||
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']) {
|
|
||||||
EventType::ORDER_COMPLETED => Shop::fulfil($event['data']['object']['id']),
|
|
||||||
EventType::SUBSCRIPTION_CANCELLED => Panel::suspend($event['data']['object']['id']),
|
|
||||||
EventType::CHECKOUT_COMPLETED => Panel::provision($event['data']['object']['subscription_ids']),
|
|
||||||
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 (API tokens)
|
### Implementing Auth (API tokens)
|
||||||
@@ -408,70 +658,36 @@ $anikeenId = $anikeenId->withClientSecret('abc123');
|
|||||||
$anikeenId = $anikeenId->withToken('abcdef123456');
|
$anikeenId = $anikeenId->withToken('abcdef123456');
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Error handling for an unsuccessful query:
|
#### Results and resources
|
||||||
|
|
||||||
|
Low-level methods on `AnikeenId` (e.g. `getAuthedUser()`, `createOrderPreview()`,
|
||||||
|
`refreshToken()`) return a `Result`. Check it before you use it:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$result = $anikeenId->sshKeysByUserId('someInvalidId');
|
$result = $anikeenId->getAuthedUser();
|
||||||
|
|
||||||
// Check, if the query was successfully
|
|
||||||
if (!$result->success()) {
|
if (!$result->success()) {
|
||||||
die('Ooops: ' . $result->error());
|
die('Ooops: ' . $result->error());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$user = $result->shift(); // the object, or the first item of a list
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Shift result to get single key data:
|
Resource methods (`$user->orders()`, `$anikeenId->sshKeysByUserId()`, …) return objects and throw
|
||||||
|
`ResourceException` instead (see [Errors](#errors)).
|
||||||
```php
|
|
||||||
$result = $anikeenId->sshKeysByUserId('someValidId');
|
|
||||||
|
|
||||||
$sshKey = $result->shift();
|
|
||||||
|
|
||||||
echo $sshKey->name;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
#### Get User SSH Key
|
#### Get User SSH Keys
|
||||||
|
|
||||||
|
Uses the app's own token (client credentials):
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$anikeenId = new Anikeen\IdAnikeenId();
|
$sshKeys = app(Anikeen\Id\AnikeenId::class)->sshKeysByUserId($anikeenUserId);
|
||||||
|
|
||||||
$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();
|
|
||||||
|
|
||||||
|
foreach ($sshKeys->toArray() as $sshKey) {
|
||||||
echo $sshKey->name;
|
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
|
#### OAuth Tokens
|
||||||
@@ -479,22 +695,13 @@ echo $preview->gross_total;
|
|||||||
```php
|
```php
|
||||||
$anikeenId = new Anikeen\Id\AnikeenId();
|
$anikeenId = new Anikeen\Id\AnikeenId();
|
||||||
|
|
||||||
$anikeenId->setClientId('abc123');
|
|
||||||
$anikeenId->setToken('abcdef123456');
|
$anikeenId->setToken('abcdef123456');
|
||||||
|
|
||||||
$result = $anikeenId->getAuthedUser();
|
$user = $anikeenId->getAuthedUser()->shift();
|
||||||
|
|
||||||
$user = $userResult->shift();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$anikeenId->setToken('uvwxyz456789');
|
$user = $anikeenId->withToken('uvwxyz456789')->getAuthedUser()->shift();
|
||||||
|
|
||||||
$result = $anikeenId->getAuthedUser();
|
|
||||||
```
|
|
||||||
|
|
||||||
```php
|
|
||||||
$result = $anikeenId->withToken('uvwxyz456789')->getAuthedUser();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Facade
|
#### Facade
|
||||||
@@ -502,14 +709,14 @@ $result = $anikeenId->withToken('uvwxyz456789')->getAuthedUser();
|
|||||||
```php
|
```php
|
||||||
use Anikeen\Id\Facades\AnikeenId;
|
use Anikeen\Id\Facades\AnikeenId;
|
||||||
|
|
||||||
AnikeenId::withClientId('abc123')->withToken('abcdef123456')->getAuthedUser();
|
AnikeenId::withToken('abcdef123456')->getAuthedUser();
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
<!-- GENERATED-DOCS -->
|
<!-- GENERATED-DOCS -->
|
||||||
|
|
||||||
[**OAuth Scopes Enums**](https://github.com/anikeen-com/id/blob/main/src/Enums/Scope.php)
|
[**OAuth Scopes Enums**](https://github.com/anikeen-com/id/blob/main/src/Id/Enums/Scope.php)
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user