Skip to content

Commit aaba7de

Browse files
committed
PayPal Plus: Getting started with webhooks (IPN not supported by rest API)
1 parent 6c3d3cb commit aaba7de

13 files changed

Lines changed: 568 additions & 181 deletions

File tree

src/Plugins/SmartStore.PayPal/Controllers/PayPalPlusController.cs

Lines changed: 22 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,18 @@
1717
using SmartStore.Services.Localization;
1818
using SmartStore.Services.Orders;
1919
using SmartStore.Services.Payments;
20+
using SmartStore.Web.Framework;
2021
using SmartStore.Web.Framework.Controllers;
2122
using SmartStore.Web.Framework.Plugins;
2223
using SmartStore.Web.Framework.Security;
2324
using SmartStore.Web.Framework.Settings;
2425

2526
namespace SmartStore.PayPal.Controllers
2627
{
27-
public class PayPalPlusController : PaymentControllerBase
28+
public class PayPalPlusController : PayPalRestApiControllerBase<PayPalPlusPaymentSettings>
2829
{
2930
private readonly HttpContextBase _httpContext;
3031
private readonly PluginMediator _pluginMediator;
31-
private readonly IPayPalService _payPalService;
3232
private readonly IGenericAttributeService _genericAttributeService;
3333
private readonly IPaymentService _paymentService;
3434

@@ -39,11 +39,12 @@ public PayPalPlusController(
3939
IOrderProcessingService orderProcessingService,
4040
PluginMediator pluginMediator,
4141
IPayPalService payPalService,
42-
IGenericAttributeService genericAttributeService)
42+
IGenericAttributeService genericAttributeService) : base(
43+
PayPalPlusProvider.SystemName,
44+
payPalService)
4345
{
4446
_httpContext = httpContext;
4547
_pluginMediator = pluginMediator;
46-
_payPalService = payPalService;
4748
_genericAttributeService = genericAttributeService;
4849
_paymentService = paymentService;
4950
}
@@ -141,10 +142,12 @@ public ActionResult Configure()
141142
ConfigGroups = T("Plugins.SmartStore.PayPal.ConfigGroups").Text.SplitSafe(";")
142143
};
143144

144-
model.AvailableSecurityProtocols = PayPalService.GetSecurityProtocols()
145+
model.AvailableSecurityProtocols = PayPal.Services.PayPalService.GetSecurityProtocols()
145146
.Select(x => new SelectListItem { Value = ((int)x.Key).ToString(), Text = x.Value })
146147
.ToList();
147148

149+
model.AvailableWebhookValidations = PayPalWebhookValidation.Simple.ToSelectList(false);
150+
148151
// it's better to also offer inactive methods here but filter them out in frontend
149152
var methods = _paymentService.LoadAllPaymentMethods(storeScope);
150153

@@ -196,38 +199,6 @@ public ActionResult Configure(PayPalPlusConfigurationModel model, FormCollection
196199
return Configure();
197200
}
198201

199-
[AdminAuthorize]
200-
public ActionResult UpsertExperienceProfile(string profileId)
201-
{
202-
var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext);
203-
var settings = Services.Settings.LoadSetting<PayPalPlusPaymentSettings>(storeScope);
204-
205-
var store = Services.StoreService.GetStoreById(storeScope == 0 ? Services.StoreContext.CurrentStore.Id : storeScope);
206-
var session = new PayPalSessionData();
207-
208-
var result = _payPalService.EnsureAccessToken(session, settings);
209-
if (result.Success)
210-
{
211-
result = _payPalService.UpsertCheckoutExperience(settings, session, store, profileId);
212-
if (result.Success && result.Id.HasValue())
213-
{
214-
settings.ExperienceProfileId = result.Id;
215-
216-
Services.Settings.SaveSetting(settings, storeScope);
217-
}
218-
else
219-
{
220-
NotifyError(result.ErrorMessage);
221-
}
222-
}
223-
else
224-
{
225-
NotifyError(result.ErrorMessage);
226-
}
227-
228-
return RedirectToAction("ConfigureProvider", "Plugin", new { area = "admin", systemName = PayPalPlusProvider.SystemName });
229-
}
230-
231202
public ActionResult PaymentInfo()
232203
{
233204
return new EmptyResult();
@@ -268,14 +239,14 @@ public ActionResult PaymentWall()
268239

269240
if (session.PaymentId.IsEmpty() || session.ApprovalUrl.IsEmpty())
270241
{
271-
var result = _payPalService.EnsureAccessToken(session, settings);
242+
var result = PayPalService.EnsureAccessToken(session, settings);
272243
if (result.Success)
273244
{
274245
var protocol = (store.SslEnabled ? "https" : "http");
275246
var returnUrl = Url.Action("CheckoutReturn", "PayPalPlus", new { area = Plugin.SystemName }, protocol);
276247
var cancelUrl = Url.Action("CheckoutCancel", "PayPalPlus", new { area = Plugin.SystemName }, protocol);
277248

278-
result = _payPalService.CreatePayment(settings, session, cart, PayPalPlusProvider.SystemName, returnUrl, cancelUrl);
249+
result = PayPalService.CreatePayment(settings, session, cart, PayPalPlusProvider.SystemName, returnUrl, cancelUrl);
279250
if (result.Success && result.Json != null)
280251
{
281252
foreach (var link in result.Json.links)
@@ -304,6 +275,18 @@ public ActionResult PaymentWall()
304275
return View(model);
305276
}
306277

278+
public ActionResult CheckoutCompleted()
279+
{
280+
var instruct = _httpContext.Session[PayPalPlusProvider.CheckoutCompletedKey] as string;
281+
282+
if (instruct.HasValue())
283+
{
284+
return Content(instruct);
285+
}
286+
287+
return new EmptyResult();
288+
}
289+
307290
[ValidateInput(false)]
308291
public ActionResult CheckoutReturn(string systemName, string paymentId, string PayerID)
309292
{
@@ -343,17 +326,5 @@ public ActionResult CheckoutCancel()
343326
// back to where he came from
344327
return RedirectToAction("PaymentMethod", "Checkout", new { area = "" });
345328
}
346-
347-
public ActionResult CheckoutCompleted()
348-
{
349-
var instruct = _httpContext.Session[PayPalPlusProvider.CheckoutCompletedKey] as string;
350-
351-
if (instruct.HasValue())
352-
{
353-
return Content(instruct);
354-
}
355-
356-
return new EmptyResult();
357-
}
358329
}
359330
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
using System.IO;
2+
using System.Web.Mvc;
3+
using SmartStore.Core.Configuration;
4+
using SmartStore.PayPal.Services;
5+
using SmartStore.PayPal.Settings;
6+
using SmartStore.Web.Framework.Controllers;
7+
using SmartStore.Web.Framework.Security;
8+
9+
namespace SmartStore.PayPal.Controllers
10+
{
11+
public abstract class PayPalRestApiControllerBase<TSetting> : PaymentControllerBase where TSetting : PayPalApiSettingsBase, ISettings, new()
12+
{
13+
public PayPalRestApiControllerBase(
14+
string systemName,
15+
IPayPalService payPalService)
16+
{
17+
SystemName = systemName;
18+
PayPalService = payPalService;
19+
}
20+
21+
private string GetControllerName()
22+
{
23+
return GetType().Name.Replace("Controller", "");
24+
}
25+
26+
protected string SystemName { get; private set; }
27+
protected IPayPalService PayPalService { get; private set; }
28+
29+
[AdminAuthorize]
30+
public ActionResult UpsertExperienceProfile()
31+
{
32+
var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext);
33+
var settings = Services.Settings.LoadSetting<TSetting>(storeScope);
34+
35+
var store = Services.StoreService.GetStoreById(storeScope == 0 ? Services.StoreContext.CurrentStore.Id : storeScope);
36+
var session = new PayPalSessionData();
37+
38+
var result = PayPalService.EnsureAccessToken(session, settings);
39+
if (result.Success)
40+
{
41+
result = PayPalService.UpsertCheckoutExperience(settings, session, store);
42+
if (result.Success && result.Id.HasValue())
43+
{
44+
settings.ExperienceProfileId = result.Id;
45+
Services.Settings.SaveSetting(settings, x => x.ExperienceProfileId, storeScope, false);
46+
Services.Settings.ClearCache();
47+
}
48+
}
49+
50+
if (result.Success)
51+
NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed"));
52+
else
53+
NotifyError(result.ErrorMessage);
54+
55+
return RedirectToAction("ConfigureProvider", "Plugin", new { area = "admin", systemName = SystemName });
56+
}
57+
58+
[AdminAuthorize]
59+
public ActionResult DeleteExperienceProfile()
60+
{
61+
var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext);
62+
var settings = Services.Settings.LoadSetting<TSetting>(storeScope);
63+
var session = new PayPalSessionData();
64+
65+
var result = PayPalService.EnsureAccessToken(session, settings);
66+
if (result.Success)
67+
{
68+
result = PayPalService.DeleteCheckoutExperience(settings, session);
69+
if (result.Success)
70+
{
71+
settings.ExperienceProfileId = null;
72+
Services.Settings.SaveSetting(settings, x => x.ExperienceProfileId, storeScope, false);
73+
Services.Settings.ClearCache();
74+
}
75+
}
76+
77+
if (result.Success)
78+
NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed"));
79+
else
80+
NotifyError(result.ErrorMessage);
81+
82+
return RedirectToAction("ConfigureProvider", "Plugin", new { area = "admin", systemName = SystemName });
83+
}
84+
85+
[AdminAuthorize]
86+
public ActionResult CreateWebhook()
87+
{
88+
var settings = Services.Settings.LoadSetting<TSetting>();
89+
var session = new PayPalSessionData();
90+
91+
if (settings.WebhookId.HasValue())
92+
{
93+
var unused = PayPalService.DeleteWebhook(settings, session);
94+
95+
Services.Settings.SaveSetting(settings, x => x.WebhookId, 0, false);
96+
}
97+
98+
var url = Url.Action("Webhook", GetControllerName(), new { area = Plugin.SystemName }, "https");
99+
100+
var result = PayPalService.EnsureAccessToken(session, settings);
101+
if (result.Success)
102+
{
103+
result = PayPalService.CreateWebhook(settings, session, url);
104+
if (result.Success)
105+
{
106+
settings.WebhookId = result.Id;
107+
Services.Settings.SaveSetting(settings, x => x.WebhookId, 0, false);
108+
}
109+
}
110+
111+
Services.Settings.ClearCache();
112+
113+
if (result.Success)
114+
NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed"));
115+
else
116+
NotifyError(result.ErrorMessage);
117+
118+
return RedirectToAction("ConfigureProvider", "Plugin", new { area = "admin", systemName = SystemName });
119+
}
120+
121+
[AdminAuthorize]
122+
public ActionResult DeleteWebhook()
123+
{
124+
var settings = Services.Settings.LoadSetting<TSetting>();
125+
var session = new PayPalSessionData();
126+
127+
if (settings.WebhookId.HasValue())
128+
{
129+
var result = PayPalService.EnsureAccessToken(session, settings);
130+
if (result.Success)
131+
{
132+
result = PayPalService.DeleteWebhook(settings, session);
133+
if (result.Success)
134+
{
135+
settings.WebhookId = null;
136+
Services.Settings.SaveSetting(settings, x => x.WebhookId, 0, false);
137+
Services.Settings.ClearCache();
138+
}
139+
}
140+
141+
if (result.Success)
142+
NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed"));
143+
else
144+
NotifyError(result.ErrorMessage);
145+
}
146+
147+
return RedirectToAction("ConfigureProvider", "Plugin", new { area = "admin", systemName = SystemName });
148+
}
149+
150+
[ValidateInput(false)]
151+
public ActionResult Webhook()
152+
{
153+
string json = null;
154+
using (var reader = new StreamReader(Request.InputStream))
155+
{
156+
json = reader.ReadToEnd();
157+
}
158+
159+
var settings = Services.Settings.LoadSetting<TSetting>();
160+
161+
var result = PayPalService.ProcessWebhook(settings, Request.Headers, json);
162+
163+
return new HttpStatusCodeResult(result);
164+
}
165+
}
166+
}

src/Plugins/SmartStore.PayPal/Localization/resources.de-de.xml

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,19 @@
2222
<LocaleResource Name="Other">
2323
<Value>Sonstiges</Value>
2424
</LocaleResource>
25+
<LocaleResource Name="PayUponInvoiceLegalNote">
26+
<Value>{0} hat die Forderung gegen Sie im Rahmen eines laufenden Factoringvertrages an die PayPal (Europe) S.àr.l. et Cie, S.C.A. abgetreten. Zahlungen mit schuldbefreiender Wirkung können nur an die PayPal (Europe) S.àr.l. et Cie, S.C.A. geleistet werden.</Value>
27+
</LocaleResource>
28+
<LocaleResource Name="ManualBankTransferNote">
29+
<Value>Bitte überweisen Sie den fälligen Betrag auf folgendes Konto.</Value>
30+
</LocaleResource>
31+
<LocaleResource Name="PaymentInstructionStrings">
32+
<Value>Referenz;Bankleitzahl;Bank;BIC;IBAN;Kontoinhaber;Kontonummer;Betrag;Zahlung fällig am;Details</Value>
33+
</LocaleResource>
34+
<LocaleResource Name="WebhookValidationFailed">
35+
<Value>Validierung einer Webhook-Nachricht ist fehlgeschlagen</Value>
36+
</LocaleResource>
37+
2538
<LocaleResource Name="ClientId">
2639
<Value>Client-ID</Value>
2740
</LocaleResource>
@@ -43,17 +56,26 @@
4356
<LocaleResource Name="ExperienceProfileId.Hint">
4457
<Value>Legt die Experience Profil-ID fest. Das Profil beinhaltet globale Daten wie z.B. Shop-Name und -Logo. Ein Profil braucht nur einmalig angelegt werden. Über den Button können Sie ein neues Profil erstellen oder ein vorhandenes aktualisieren.</Value>
4558
</LocaleResource>
46-
<LocaleResource Name="PaymentExecuteFailed">
47-
<Value>Die Ausführung der Zahlung wurde durch PayPal abgelehnt.</Value>
59+
<LocaleResource Name="ExperienceProfileId">
60+
<Value>Experience Profil-ID</Value>
4861
</LocaleResource>
49-
<LocaleResource Name="PayUponInvoiceLegalNote">
50-
<Value>{0} hat die Forderung gegen Sie im Rahmen eines laufenden Factoringvertrages an die PayPal (Europe) S.àr.l. et Cie, S.C.A. abgetreten. Zahlungen mit schuldbefreiender Wirkung können nur an die PayPal (Europe) S.àr.l. et Cie, S.C.A. geleistet werden.</Value>
62+
<LocaleResource Name="WebhookId">
63+
<Value>Webhook-ID</Value>
5164
</LocaleResource>
52-
<LocaleResource Name="ManualBankTransferNote">
53-
<Value>Bitte überweisen Sie den fälligen Betrag auf folgendes Konto.</Value>
65+
<LocaleResource Name="WebhookId.Hint">
66+
<Value>Legt die Webhook-ID fest. Über einen Webhook sendet PayPal Statusänderungen von Zahlungen an Ihren Shop, wodurch der Zahlungsstatus Ihrer Aufträge automatisch aktualisert werden kann.</Value>
5467
</LocaleResource>
55-
<LocaleResource Name="PaymentInstructionStrings">
56-
<Value>Referenz;Bankleitzahl;Bank;BIC;IBAN;Kontoinhaber;Kontonummer;Betrag;Zahlung fällig am;Details</Value>
68+
<LocaleResource Name="WebhookValidation">
69+
<Value>Validierung von Nachrichten</Value>
70+
</LocaleResource>
71+
<LocaleResource Name="WebhookValidation.Hint">
72+
<Value>Legt fest, ob und wie Webhook-Nachrichten validiert werden sollen.</Value>
73+
</LocaleResource>
74+
<LocaleResource Name="Enums.SmartStore.PayPal.Services.PayPalWebhookValidation.None" AppendRootKey="false">
75+
<Value>Keine</Value>
76+
</LocaleResource>
77+
<LocaleResource Name="Enums.SmartStore.PayPal.Services.PayPalWebhookValidation.Simple" AppendRootKey="false">
78+
<Value>Einfach</Value>
5779
</LocaleResource>
5880

5981
<LocaleResource Name="Plugins.Payments.PayPal" AppendRootKey="false">

0 commit comments

Comments
 (0)