Skip to content

Commit 07ed68e

Browse files
committed
42083 V14: Integrations (Shopify)
- Add new API - Add UI for shopify property editor setting: Authorization
1 parent 2a4896a commit 07ed68e

36 files changed

+911
-333
lines changed

src/Umbraco.Cms.Integrations.Commerce.Shopify/Api/Management/Controllers/CheckConfigurationController.cs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,28 @@ namespace Umbraco.Cms.Integrations.Commerce.Shopify.Core.Api.Management.Controll
1919
[ApiExplorerSettings(GroupName = Constants.ManagementApi.GroupName)]
2020
public class CheckConfigurationController : ShopifyControllerBase
2121
{
22-
public CheckConfigurationController(IOptions<ShopifySettings> shopifySettings, IShopifyService shopifyService, IShopifyAuthorizationService shopifyAuthorizationService) : base(shopifySettings, shopifyService, shopifyAuthorizationService)
22+
private readonly ShopifyOAuthSettings _oauthSettings;
23+
public CheckConfigurationController(IOptions<ShopifySettings> shopifySettings, IShopifyService shopifyService, IShopifyAuthorizationService shopifyAuthorizationService, IOptions<ShopifyOAuthSettings> oauthSettings) : base(shopifySettings, shopifyService, shopifyAuthorizationService)
2324
{
25+
_oauthSettings = oauthSettings.Value;
2426
}
2527

2628
[HttpGet("check-configuration")]
2729
[ProducesResponseType(typeof(EditorSettings), StatusCodes.Status200OK)]
28-
public ActionResult CheckConfiguration()
30+
public IActionResult CheckConfiguration()
2931
{
30-
var setting = ShopifyService.GetApiConfiguration();
31-
return Ok(setting);
32+
var settings = !string.IsNullOrEmpty(ShopifySettings.AccessToken)
33+
? new EditorSettings { IsValid = true, Type = ConfigurationType.Api }
34+
: ShopifySettings.UseUmbracoAuthorization
35+
? new EditorSettings { IsValid = true, Type = ConfigurationType.OAuth }
36+
: !string.IsNullOrEmpty(_oauthSettings.ClientId)
37+
&& !string.IsNullOrEmpty(_oauthSettings.Scopes)
38+
&& !string.IsNullOrEmpty(_oauthSettings.ClientSecret)
39+
&& !string.IsNullOrEmpty(_oauthSettings.TokenEndpoint)
40+
? new EditorSettings { IsValid = true, Type = ConfigurationType.OAuth }
41+
: new EditorSettings();
42+
43+
return Ok(settings);
3244
}
3345
}
3446
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using Asp.Versioning;
2+
using Microsoft.AspNetCore.Authorization;
3+
using Microsoft.AspNetCore.Http;
4+
using Microsoft.AspNetCore.Mvc;
5+
using Microsoft.Extensions.Options;
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Linq;
9+
using System.Text;
10+
using System.Threading.Tasks;
11+
using Umbraco.Cms.Integrations.Commerce.Shopify.Core;
12+
using Umbraco.Cms.Integrations.Commerce.Shopify.Core.Api.Management.Controllers;
13+
using Umbraco.Cms.Integrations.Commerce.Shopify.Core.Configuration;
14+
using Umbraco.Cms.Integrations.Commerce.Shopify.Core.Services;
15+
16+
namespace Umbraco.Cms.Integrations.Commerce.Shopify.Api.Management.Controllers
17+
{
18+
[ApiVersion("1.0")]
19+
[ApiExplorerSettings(GroupName = Constants.ManagementApi.GroupName)]
20+
public class RefreshAccessTokenController : ShopifyControllerBase
21+
{
22+
public RefreshAccessTokenController(IOptions<ShopifySettings> shopifySettings, IShopifyService shopifyService, IShopifyAuthorizationService shopifyAuthorizationService) : base(shopifySettings, shopifyService, shopifyAuthorizationService)
23+
{
24+
}
25+
26+
[HttpPost("refresh")]
27+
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
28+
public async Task<IActionResult> RefreshAccessToken()
29+
{
30+
var response = await ShopifyAuthorizationService.RefreshAccessTokenAsync();
31+
return Ok(response);
32+
}
33+
}
34+
}

src/Umbraco.Cms.Integrations.Commerce.Shopify/Api/Management/Controllers/ShopifyControllerBase.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public abstract class ShopifyControllerBase : Controller
2626
protected IShopifyService ShopifyService;
2727
protected IShopifyAuthorizationService ShopifyAuthorizationService;
2828

29+
2930
protected ShopifyControllerBase(IOptions<ShopifySettings> shopifySettings, IShopifyService shopifyService, IShopifyAuthorizationService shopifyAuthorizationService)
3031
{
3132
ShopifySettings = shopifySettings.Value;

src/Umbraco.Cms.Integrations.Commerce.Shopify/Client/generated/services.gen.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import type { CancelablePromise } from './core/CancelablePromise';
44
import { OpenAPI } from './core/OpenAPI';
55
import { request as __request } from './core/request';
6-
import type { CheckConfigurationResponse, GetAccessTokenData, GetAccessTokenResponse, GetAuthorizationUrlResponse, GetListData, GetListResponse, GetListByIdsData, GetListByIdsResponse, RevokeAccessTokenResponse, GetTotalPagesResponse, ValidateAccessTokenResponse } from './types.gen';
6+
import type { CheckConfigurationResponse, GetAccessTokenData, GetAccessTokenResponse, GetAuthorizationUrlResponse, GetListData, GetListResponse, GetListByIdsData, GetListByIdsResponse, RefreshAccessTokenResponse, RevokeAccessTokenResponse, GetTotalPagesResponse, ValidateAccessTokenResponse } from './types.gen';
77

88
export class ShopifyService {
99
/**
@@ -89,6 +89,20 @@ export class ShopifyService {
8989
});
9090
}
9191

92+
/**
93+
* @returns string OK
94+
* @throws ApiError
95+
*/
96+
public static refreshAccessToken(): CancelablePromise<RefreshAccessTokenResponse> {
97+
return __request(OpenAPI, {
98+
method: 'POST',
99+
url: '/umbraco/shopify/management/api/v1/refresh',
100+
errors: {
101+
401: 'The resource is protected and requires an authentication token'
102+
}
103+
});
104+
}
105+
92106
/**
93107
* @returns string OK
94108
* @throws ApiError

src/Umbraco.Cms.Integrations.Commerce.Shopify/Client/generated/types.gen.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ export type GetListByIdsData = {
9090

9191
export type GetListByIdsResponse = ResponseDtoProductsListDtoModel;
9292

93+
export type RefreshAccessTokenResponse = string;
94+
9395
export type RevokeAccessTokenResponse = string;
9496

9597
export type GetTotalPagesResponse = number;
@@ -170,6 +172,20 @@ export type $OpenApiTs = {
170172
};
171173
};
172174
};
175+
'/umbraco/shopify/management/api/v1/refresh': {
176+
post: {
177+
res: {
178+
/**
179+
* OK
180+
*/
181+
200: string;
182+
/**
183+
* The resource is protected and requires an authentication token
184+
*/
185+
401: unknown;
186+
};
187+
};
188+
};
173189
'/umbraco/shopify/management/api/v1/revoke-access-token': {
174190
post: {
175191
res: {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { UmbElementMixin } from "@umbraco-cms/backoffice/element-api";
2+
import { LitElement, customElement, html } from "@umbraco-cms/backoffice/external/lit";
3+
import { SHOPIFY_CONTEXT_TOKEN } from "../../context/shopify.context";
4+
5+
const elementName = "shopify-amount";
6+
7+
@customElement(elementName)
8+
export class ShopifyAmountElement extends UmbElementMixin(LitElement){
9+
#shopifyContext!: typeof SHOPIFY_CONTEXT_TOKEN.TYPE;
10+
11+
render() {
12+
return html`
13+
<div>
14+
<uui-input></uui-input>
15+
<span>-</span>
16+
<uui-input placeholder=""></uui-input>
17+
</div>
18+
`;
19+
}
20+
}
21+
22+
export default ShopifyAmountElement;
23+
24+
declare global {
25+
interface HTMLElementTagNameMap {
26+
[elementName]: ShopifyAmountElement;
27+
}
28+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { ManifestPropertyEditorUi } from "@umbraco-cms/backoffice/extension-registry";
2+
3+
export const manifests : ManifestPropertyEditorUi = {
4+
type: 'propertyEditorUi',
5+
alias: 'Shopify.PropertyEditorUi.Amount',
6+
name: 'Shopify Product Picker Amount Setting',
7+
element: () => import('./amount-property-editor.element.js'),
8+
meta: {
9+
label: 'Amount',
10+
icon: 'icon-autofill',
11+
group: 'common',
12+
}
13+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import { UmbElementMixin } from "@umbraco-cms/backoffice/element-api";
2+
import { LitElement, customElement, html, property, state, when } from "@umbraco-cms/backoffice/external/lit";
3+
import { SHOPIFY_CONTEXT_TOKEN } from "../../context/shopify.context";
4+
import { ConfigDescription, ShopifyOAuthSetup, type ShopifyServiceStatus } from "../../models/shopify-service.model";
5+
import { OAuthRequestDtoModel } from "../../../generated";
6+
import { UMB_NOTIFICATION_CONTEXT, type UmbNotificationColor } from "@umbraco-cms/backoffice/notification";
7+
8+
const elementName = "shopify-authorization";
9+
10+
@customElement(elementName)
11+
export class ShopifyAuthorizationElement extends UmbElementMixin(LitElement){
12+
#shopifyContext!: typeof SHOPIFY_CONTEXT_TOKEN.TYPE;
13+
14+
@state()
15+
private _serviceStatus: ShopifyServiceStatus = {
16+
isValid: false,
17+
type: "",
18+
description: "",
19+
useOAuth: false
20+
};
21+
22+
@state()
23+
private _oauthSetup: ShopifyOAuthSetup = {
24+
isConnected: false,
25+
isAccessTokenExpired: false,
26+
isAccessTokenValid: false
27+
};
28+
29+
@property({ type: String })
30+
public value = "";
31+
32+
constructor() {
33+
super();
34+
35+
this.consumeContext(SHOPIFY_CONTEXT_TOKEN, (context) => {
36+
this.#shopifyContext = context;
37+
});
38+
}
39+
40+
async connectedCallback() {
41+
super.connectedCallback();
42+
await this.#checkApiConfiguration();
43+
}
44+
45+
async #checkApiConfiguration() {
46+
const { data } = await this.#shopifyContext.checkConfiguration();
47+
if (!data || !data.type?.value) return;
48+
49+
this._serviceStatus = {
50+
isValid: data.isValid,
51+
type: data.type.value,
52+
description: this.#getDescription(data.type.value),
53+
useOAuth: data.isValid && data.type.value === "OAuth"
54+
}
55+
56+
if (this._serviceStatus.useOAuth) {
57+
await this.#validateOAuthSetup();
58+
}
59+
60+
if (!data!.isValid) {
61+
this._showError("Invalid setup. Please review the API/OAuth settings.");
62+
}
63+
}
64+
65+
async #validateOAuthSetup() {
66+
const { data } = await this.#shopifyContext.validateAccessToken();
67+
if (data) {
68+
this._oauthSetup = {
69+
isConnected: data.isValid,
70+
isAccessTokenExpired: data.isExpired,
71+
isAccessTokenValid: data.isValid
72+
}
73+
74+
if (this._oauthSetup.isConnected && this._oauthSetup.isAccessTokenValid) {
75+
this._serviceStatus.description = ConfigDescription.oauthConnected;
76+
}
77+
78+
if (this._oauthSetup.isAccessTokenExpired) {
79+
await this.#shopifyContext.refreshAccessToken();
80+
}
81+
}
82+
}
83+
84+
#getDescription(type: string) {
85+
switch (type) {
86+
case "API": return ConfigDescription.api;
87+
case "OAuth": return ConfigDescription.oauth;
88+
case "OAuthConnected": return ConfigDescription.oauthConnected;
89+
default: return ConfigDescription.none;
90+
}
91+
}
92+
93+
private async _showSuccess(message: string) {
94+
await this._showMessage(message, "positive");
95+
}
96+
97+
private async _showError(message: string) {
98+
await this._showMessage(message, "danger");
99+
}
100+
101+
private async _showMessage(message: string, color: UmbNotificationColor) {
102+
const notificationContext = await this.getContext(UMB_NOTIFICATION_CONTEXT);
103+
notificationContext?.peek(color, {
104+
data: { message },
105+
});
106+
}
107+
108+
async #connectButtonClick(){
109+
window.addEventListener("message", async (event: MessageEvent) => {
110+
if (event.data.type === "hubspot:oauth:success") {
111+
112+
const oauthRequestDtoModel: OAuthRequestDtoModel = {
113+
code: event.data.code
114+
};
115+
116+
const { data } = await this.#shopifyContext.getAccessToken(oauthRequestDtoModel);
117+
if (!data) return;
118+
119+
if (data.startsWith("Error:")) {
120+
this._showError(data);
121+
} else {
122+
this._oauthSetup = {
123+
isConnected: true
124+
};
125+
this._serviceStatus.description = ConfigDescription.oauthConnected;
126+
this._showSuccess("OAuth Connected");
127+
128+
}
129+
130+
}
131+
}, false);
132+
133+
const { data } = await this.#shopifyContext.getAuthorizationUrl();
134+
if (!data) return;
135+
136+
window.open(data, "Authorize", "width=900,height=700,modal=yes,alwaysRaised=yes");
137+
}
138+
139+
async #revokeButtonClick(){
140+
await this.#shopifyContext.revokeAccessToken();
141+
142+
this._oauthSetup = {
143+
isConnected: false
144+
};
145+
this._serviceStatus.description = ConfigDescription.none;
146+
this._showSuccess("OAuth connection revoked.");
147+
}
148+
149+
render() {
150+
return html`
151+
<div>
152+
<p>${this._serviceStatus.description}</p>
153+
</div>
154+
${when(this._serviceStatus.useOAuth, () =>
155+
html`
156+
<div>
157+
<uui-button
158+
look="primary"
159+
label="Connect"
160+
?disabled=${this._oauthSetup.isConnected}
161+
.onclick=${this.#connectButtonClick()}></uui-button>
162+
<uui-button
163+
color="danger"
164+
look="secondary"
165+
label="Revoke"
166+
?disabled=${!this._oauthSetup.isConnected}
167+
.onclick=${this.#revokeButtonClick()}></uui-button>
168+
</div>
169+
`)}
170+
171+
`;
172+
}
173+
}
174+
175+
export default ShopifyAuthorizationElement;
176+
177+
declare global {
178+
interface HTMLElementTagNameMap {
179+
[elementName]: ShopifyAuthorizationElement;
180+
}
181+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { ManifestPropertyEditorUi } from "@umbraco-cms/backoffice/extension-registry";
2+
3+
export const manifests : ManifestPropertyEditorUi = {
4+
type: 'propertyEditorUi',
5+
alias: 'Shopify.PropertyEditorUi.Authorization',
6+
name: 'Shopify Product Picker Authorization Setting',
7+
element: () => import('./authorization-property-editor.element.js'),
8+
meta: {
9+
label: 'Authorization',
10+
icon: 'icon-autofill',
11+
group: 'common',
12+
}
13+
}

src/Umbraco.Cms.Integrations.Commerce.Shopify/Client/src/context/shopify.context.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ export class ShopifyContext extends UmbControllerBase{
4545
async getAuthorizationUrl(){
4646
return await this.#repository.getAuthorizationUrl();
4747
}
48+
49+
async refreshAccessToken(){
50+
return await this.#repository.refreshAccessToken();
51+
}
4852
}
4953

5054
export default ShopifyContext;

0 commit comments

Comments
 (0)