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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions monorepo-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ parameters:
src/LinkedIn: 'git@github.com:SocialiteProviders/LinkedIn.git'
src/Live: 'git@github.com:SocialiteProviders/Microsoft-Live.git'
src/LifeScienceLogin: 'git@github.com:SocialiteProviders/LifeScienceLogin.git'
src/LightspeedRetail: 'git@github.com:SocialiteProviders/LightspeedRetail.git'
src/MailChimp: 'git@github.com:SocialiteProviders/MailChimp.git'
src/Mailru: 'git@github.com:SocialiteProviders/Mailru.git'
src/MakerLog: 'git@github.com:SocialiteProviders/MakerLog.git'
Expand Down
19 changes: 19 additions & 0 deletions src/LightspeedRetail/LightspeedRetailExtendSocialite.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace SocialiteProviders\LightspeedRetail;

use SocialiteProviders\Manager\SocialiteWasCalled;

class LightspeedRetailExtendSocialite
{
/**
* Register the provider.
*
* @param \SocialiteProviders\Manager\SocialiteWasCalled $socialiteWasCalled
* @return void
*/
public function handle(SocialiteWasCalled $socialiteWasCalled): void
{
$socialiteWasCalled->extendSocialite('lightspeedretail', Provider::class);
}
}
203 changes: 203 additions & 0 deletions src/LightspeedRetail/Provider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
<?php

namespace SocialiteProviders\LightspeedRetail;

use GuzzleHttp\RequestOptions;
use Laravel\Socialite\Two\InvalidStateException;
use SocialiteProviders\Manager\Contracts\OAuth2\ProviderInterface;
use SocialiteProviders\Manager\OAuth2\AbstractProvider;
use SocialiteProviders\Manager\OAuth2\User;

class Provider extends AbstractProvider implements ProviderInterface
{
const IDENTIFIER = 'LIGHTSPEEDRETAIL';

protected $scopes = [''];
Copy link
Member

Choose a reason for hiding this comment

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

Can we just leave this as an empty array please?


/**
* The domain prefix for the current OAuth flow.
*
* @var string|null
*/
protected $domainPrefix = null;

protected function getAuthUrl($state)
{
return $this->buildAuthUrlFromBase('https://secure.retail.lightspeed.app/connect', $state);
}

/**
* Get the token URL for the provider.
* For the initial request, we must use the domain_prefix from the callback.
*
* @return string
*/
protected function getTokenUrl()
{
// We must get the domain_prefix from the callback for the initial token request
$domainPrefix = request()->input('domain_prefix', $this->domainPrefix);
Copy link
Member

Choose a reason for hiding this comment

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

Is it safe to get this from the url? example.com/# results in
curl 'https://example.com/#.retail.lightspeed.app/api/1.0/token'


if (empty($domainPrefix)) {
throw new \InvalidArgumentException(
'Domain prefix is required to get the token URL. Make sure it is passed in the callback.'
);
}

return "https://{$domainPrefix}.retail.lightspeed.app/api/1.0/token";
}

/**
* Get the base API URL.
*
* @return string
*/
protected function getBaseUrl()
{
return sprintf('https://%s.retail.lightspeed.app/api', $this->getDomainPrefix());
}

/**
* Get the domain prefix.
*
* @return string
*/
protected function getDomainPrefix()
{
if ($this->domainPrefix) {
return $this->domainPrefix;
}

// For subsequent calls after token retrieval
$configPrefix = $this->getConfig('domain_prefix');
if (!empty($configPrefix)) {
return $configPrefix;
}

throw new \InvalidArgumentException(
'No domain_prefix found. It should be received from the token response ' .
'or configured as a default in the config.'
);
}

/**
* {@inheritdoc}
*/
public function user()
{
if ($this->hasInvalidState()) {
throw new InvalidStateException();
}

$response = $this->getAccessTokenResponse($this->getCode());

// Store the domain_prefix from the token response
if (isset($response['domain_prefix'])) {
$this->domainPrefix = $response['domain_prefix'];
}

$user = $this->mapUserToObject($this->getUserByToken(
$token = $this->parseAccessToken($response)
));

// Store domain prefix with the user
$user->domainPrefix = $this->domainPrefix;
Copy link
Member

Choose a reason for hiding this comment

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

this should be set on the raw data right?

Copy link
Member

Choose a reason for hiding this comment

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

or, create a custom user object with this prop.


return $user->setToken($token)
->setRefreshToken($this->parseRefreshToken($response))
->setExpiresIn($this->parseExpiresIn($response));
}

/**
* {@inheritdoc}
*/
protected function getUserByToken($token)
{
$response = $this->getHttpClient()->get($this->getBaseUrl().'/2.0/user', [
RequestOptions::HEADERS => [
'Authorization' => 'Bearer '.$token,
],
]);

return json_decode((string) $response->getBody(), true)['data'] ?? [];
}

/**
* {@inheritdoc}
*/
protected function mapUserToObject(array $user)
{
return (new User)->setRaw($user)->map([
'id' => $user['id'],
'nickname' => $user['display_name'],
'name' => $user['username'],
'email' => $user['email']
]);
}

/**
* Set the domain prefix for API calls.
*
* @param string $domainPrefix
* @return $this
*/
public function setDomainPrefix($domainPrefix)
{
$this->domainPrefix = $domainPrefix;

return $this;
}

/**
* Get access token response from provider
*
* @param string $code
* @return array
*/
public function getAccessTokenResponse($code)
{
$response = parent::getAccessTokenResponse($code);

// Store the domain_prefix from the token response
if (isset($response['domain_prefix'])) {
$this->domainPrefix = $response['domain_prefix'];
}

return $response;
}

/**
* Refresh a token.
*
* @param string $refreshToken
* @return array
*/
public function refreshToken($refreshToken)
{
if (!$this->domainPrefix) {
throw new \InvalidArgumentException(
'Domain prefix must be set before refreshing tokens. Use setDomainPrefix() method.'
);
}

$refreshTokenUrl = "https://{$this->domainPrefix}.retail.lightspeed.app/api/1.0/token";

$response = $this->getHttpClient()->post($refreshTokenUrl, [
'headers' => ['Accept' => 'application/json'],
'form_params' => [
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken,
'client_id' => $this->clientId,
'client_secret' => $this->clientSecret,
],
]);

$refreshedToken = json_decode((string) $response->getBody(), true);

// Update domain prefix in case it changed
if (isset($refreshedToken['domain_prefix'])) {
$this->domainPrefix = $refreshedToken['domain_prefix'];
}

return $refreshedToken;
}
}
94 changes: 94 additions & 0 deletions src/LightspeedRetail/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Lightspeed Retail

```bash
composer require socialiteproviders/lightspeedretail
```

## Installation & Basic Usage

Please see the [Base Installation Guide](https://socialiteproviders.com/usage/), then follow the provider specific instructions below.

### Add configuration to `config/services.php`

```php
'lightspeedretail' => [
'client_id' => env('LIGHTSPEEDRETAIL_CLIENT_ID'),
'client_secret' => env('LIGHTSPEEDRETAIL_CLIENT_SECRET'),
'redirect' => env('LIGHTSPEEDRETAIL_REDIRECT_URI')
],
```

### Add provider event listener

#### Laravel 11+

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.

* Note: You do not need to add anything for the built-in socialite providers unless you override them with your own providers.

```php
Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) {
$event->extendSocialite('lightspeedretail', \SocialiteProviders\LightspeedRetail\Provider::class);
});
```
<details>
<summary>
Laravel 10 or below
</summary>
Configure the package's listener to listen for `SocialiteWasCalled` events.

Add the event to your `listen[]` array in `app/Providers/EventServiceProvider`. See the [Base Installation Guide](https://socialiteproviders.com/usage/) for detailed instructions.

```php
protected $listen = [
\SocialiteProviders\Manager\SocialiteWasCalled::class => [
// ... other providers
\SocialiteProviders\LightspeedRetail\LightspeedRetailExtendSocialite::class.'@handle',
],
];
```
</details>

### Usage

You should now be able to use the provider like you would regularly use Socialite (assuming you have the facade installed):

```php
return Socialite::driver('lightspeedretail')->redirect();
```

### Returned User fields

- ``id``
- ``nickname``
- ``name``
- ``email``
- ``domainPrefix`` (specific to Lightspeed Retail - store this for API calls)

### Domain Prefix Management

Lightspeed Retail uses a domain prefix (e.g., `example` in `example.retail.lightspeed.app`) that is unique to each retailer account. This prefix is returned in the OAuth response and must be stored and used for all future API calls.

```php
// Get user with domain prefix
$user = Socialite::driver('lightspeedretail')->user();
$domainPrefix = $user->domainPrefix;

// Store domain prefix with tokens for future use
$authUser = User::updateOrCreate([
'email' => $user->email
], [
'name' => $user->name,
'lightspeed_token' => $user->token,
'lightspeed_refresh_token' => $user->refreshToken,
'lightspeed_domain_prefix' => $user->domainPrefix,
'token_expires_at' => now()->addSeconds($user->expiresIn)
]);

// When making API calls later, set the domain prefix first
$provider = Socialite::driver('lightspeedretail')
->setDomainPrefix($authUser->lightspeed_domain_prefix);

// For refreshing tokens
$refreshedToken = $provider->refreshToken($authUser->lightspeed_refresh_token);
```
34 changes: 34 additions & 0 deletions src/LightspeedRetail/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "socialiteproviders/lightspeedretail",
"description": "LightspeedRetail OAuth2 Provider for Laravel Socialite",
"license": "MIT",
"keywords": [
"lightspeedretail",
"laravel",
"oauth",
"oauth2",
"provider",
"socialite"
],
"authors": [
{
"name": "Gilbert Paquin",
"email": "gpaquin@agencecogix.com"
}
],
"support": {
"issues": "https://github.com/socialiteproviders/providers/issues",
"source": "https://github.com/socialiteproviders/providers",
"docs": "https://socialiteproviders.com/lightspeedretail"
},
"require": {
"php": "^8.0",
"ext-json": "*",
"socialiteproviders/manager": "^4.4"
},
"autoload": {
"psr-4": {
"SocialiteProviders\\LightspeedRetail\\": ""
}
}
}
Loading