Skip to content

Commit bf771ff

Browse files
authored
Merge pull request #40 from moyasar/feat/support-samsung-pay-tokenization
2 parents 03ff22f + 3b3037e commit bf771ff

8 files changed

Lines changed: 233 additions & 0 deletions

File tree

example/src/App.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ const paymentConfig = new PaymentConfig({
4444
merchantName: 'Test Samsung Pay from app',
4545
orderNumber: 'c553ed70-fb79-487c-b3d2-15aca6aff90c',
4646
manual: false,
47+
saveCard: false,
4748
}),
4849
applyCoupon: true,
4950
// splits: [ publishableApiKey for testing: 'pk_test_uQra5pwtUo9GaenMSS4XgfAmeLhmjUTJwFdXJxsH', baseUrl: 'https://apimig.moyasar.com'

src/__tests__/__fixtures__/payment_response_fixture.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,30 @@ export const paymentResponseWithFailedStcFixture = PaymentResponse.fromJson(
8787
jsonFailed,
8888
PaymentType.stcPay
8989
);
90+
91+
export const paymentResponseWithInitSamsungJsonFixture = {
92+
...paymentResponseWithInitJsonFixture,
93+
source: {
94+
type: 'samsungpay',
95+
number: '966500000001',
96+
gateway_id: 'samsung_gateway_123',
97+
reference_number: 'ref_123',
98+
message: 'approved',
99+
token: 'samsung_token_123',
100+
},
101+
};
102+
103+
export const paymentResponseWithInitSamsungFixture = PaymentResponse.fromJson(
104+
paymentResponseWithInitSamsungJsonFixture,
105+
PaymentType.samsungPay
106+
);
107+
108+
const jsonSamsungPaid = {
109+
...paymentResponseWithInitSamsungJsonFixture,
110+
status: 'paid',
111+
};
112+
113+
export const paymentResponseWithPaidSamsungFixture = PaymentResponse.fromJson(
114+
jsonSamsungPaid,
115+
PaymentType.samsungPay
116+
);

src/__tests__/models/payment_response.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { PaymentResponse } from '../../models/api/api_responses/payment_response';
22
import { PaymentType } from '../../models/payment_type';
33
import { CreditCardResponseSource } from '../../models/api/sources/credit_card/credit_card_response_source';
4+
import { SamsungPayPaymentResponseSource } from '../../models/api/sources/samsung_pay/samsung_pay_response_source';
45

56
describe('PaymentResponse', () => {
67
it('should create a PaymentResponse instance from JSON with credit card source with all params', () => {
@@ -155,6 +156,81 @@ describe('PaymentResponse', () => {
155156
expect((paymentResponse.source as any).message).toBe(json.source.message);
156157
});
157158

159+
it('should create a PaymentResponse instance from JSON with samsung pay source with all params', () => {
160+
const json = {
161+
id: '123',
162+
status: 'paid',
163+
amount: 1000,
164+
fee: 50,
165+
currency: 'SAR',
166+
refunded: 0,
167+
captured: 1000,
168+
captured_at: '2023-01-01T00:00:00Z',
169+
voided_at: '2023-01-01T00:00:00Z',
170+
description: 'Payment for order #123',
171+
amount_format: '10.00',
172+
fee_format: '0.50',
173+
refunded_format: '0.00',
174+
captured_format: '10.00',
175+
invoice_id: '123',
176+
ip: '8.8.8.8',
177+
callback_url: 'https://example.com/callback',
178+
created_at: '2023-01-01T00:00:00Z',
179+
updated_at: '2023-01-01T00:00:00Z',
180+
metadata: { orderId: '12345' },
181+
source: {
182+
number: '123456',
183+
gateway_id: '123',
184+
reference_number: '123',
185+
message: 'message123',
186+
token: 'token123',
187+
},
188+
};
189+
190+
const paymentResponse = PaymentResponse.fromJson(
191+
json,
192+
PaymentType.samsungPay
193+
);
194+
195+
expect(paymentResponse.id).toBe(json.id);
196+
expect(paymentResponse.status).toBe(json.status);
197+
expect(paymentResponse.amount).toBe(json.amount);
198+
expect(paymentResponse.fee).toBe(json.fee);
199+
expect(paymentResponse.currency).toBe(json.currency);
200+
expect(paymentResponse.refunded).toBe(json.refunded);
201+
expect(paymentResponse.captured).toBe(json.captured);
202+
expect(paymentResponse.capturedAt).toBe(json.captured_at);
203+
expect(paymentResponse.voidedAt).toBe(json.voided_at);
204+
expect(paymentResponse.description).toBe(json.description);
205+
expect(paymentResponse.amountFormat).toBe(json.amount_format);
206+
expect(paymentResponse.feeFormat).toBe(json.fee_format);
207+
expect(paymentResponse.refundedFormat).toBe(json.refunded_format);
208+
expect(paymentResponse.capturedFormat).toBe(json.captured_format);
209+
expect(paymentResponse.invoiceId).toBe(json.invoice_id);
210+
expect(paymentResponse.ip).toBe(json.ip);
211+
expect(paymentResponse.callbackUrl).toBe(json.callback_url);
212+
expect(paymentResponse.createdAt).toBe(json.created_at);
213+
expect(paymentResponse.updatedAt).toBe(json.updated_at);
214+
expect(paymentResponse.metadata).toEqual(json.metadata);
215+
216+
expect(
217+
(paymentResponse.source as SamsungPayPaymentResponseSource).number
218+
).toBe(json.source.number);
219+
expect(
220+
(paymentResponse.source as SamsungPayPaymentResponseSource).gatewayId
221+
).toBe(json.source.gateway_id);
222+
expect(
223+
(paymentResponse.source as SamsungPayPaymentResponseSource)
224+
.referenceNumber
225+
).toBe(json.source.reference_number);
226+
expect(
227+
(paymentResponse.source as SamsungPayPaymentResponseSource).message
228+
).toBe(json.source.message);
229+
expect(
230+
(paymentResponse.source as SamsungPayPaymentResponseSource).token
231+
).toBe(json.source.token);
232+
});
233+
158234
it('should create a PaymentResponse instance from JSON with unknown source with all params', () => {
159235
const json = {
160236
id: '123',
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { PaymentRequest } from '../../models/api/api_requests/payment_request';
2+
import { GeneralError, NetworkError } from '../../models/errors/moyasar_errors';
3+
import { createPayment } from '../../services/payment_service';
4+
import { SamsungPayRequestSource } from '../../models/api/sources/samsung_pay/samsung_pay_request_source';
5+
import { onSamsungPayResponse } from '../../views/samsung_pay/samsung_pay';
6+
import { paymentConfigWithoutSaveOnlyFixture } from '../__fixtures__/payment_config_fixture';
7+
import { SamsungPayConfig } from '../../models/samsung_pay_config';
8+
import { paymentResponseWithPaidSamsungFixture } from '../__fixtures__/payment_response_fixture';
9+
10+
jest.mock('../../services/payment_service');
11+
jest.mock('../../localizations/i18n', () => ({
12+
getCurrentLang: jest.fn(() => 'en'),
13+
}));
14+
15+
describe('onSamsungPayResponse', () => {
16+
const mockSamsungPayToken = 'mockSamsungPayToken';
17+
const mockOrderNumber = 'order-number-123';
18+
const onPaymentResult = jest.fn();
19+
20+
beforeEach(() => {
21+
jest.resetAllMocks();
22+
});
23+
24+
it('should create a payment and call onPaymentResult with the response', async () => {
25+
(createPayment as jest.Mock).mockResolvedValue(
26+
paymentResponseWithPaidSamsungFixture
27+
);
28+
29+
await onSamsungPayResponse(
30+
mockSamsungPayToken,
31+
mockOrderNumber,
32+
paymentConfigWithoutSaveOnlyFixture,
33+
onPaymentResult
34+
);
35+
36+
expect(createPayment).toHaveBeenCalledWith(
37+
expect.any(PaymentRequest),
38+
paymentConfigWithoutSaveOnlyFixture.publishableApiKey
39+
);
40+
expect(onPaymentResult).toHaveBeenCalledWith(
41+
paymentResponseWithPaidSamsungFixture
42+
);
43+
});
44+
45+
it('should pass manual and save_card from samsung config to source payload', async () => {
46+
(createPayment as jest.Mock).mockResolvedValue(
47+
paymentResponseWithPaidSamsungFixture
48+
);
49+
50+
const configWithSamsungTokenization = {
51+
...paymentConfigWithoutSaveOnlyFixture,
52+
samsungPay: new SamsungPayConfig({
53+
serviceId: 'ea810dafb758408fa530b1',
54+
merchantName: 'Test Samsung',
55+
orderNumber: mockOrderNumber,
56+
manual: true,
57+
saveCard: true,
58+
}),
59+
};
60+
61+
await onSamsungPayResponse(
62+
mockSamsungPayToken,
63+
mockOrderNumber,
64+
configWithSamsungTokenization,
65+
onPaymentResult
66+
);
67+
68+
const paymentRequest = (createPayment as jest.Mock).mock
69+
.calls[0][0] as PaymentRequest;
70+
const samsungSource = paymentRequest.source as SamsungPayRequestSource;
71+
72+
expect(samsungSource.toJson()).toEqual(
73+
expect.objectContaining({
74+
type: 'samsungpay',
75+
token: mockSamsungPayToken,
76+
manual: 'true',
77+
save_card: true,
78+
})
79+
);
80+
expect(paymentRequest.metadata).toEqual(
81+
expect.objectContaining({ samsungpay_order_id: mockOrderNumber })
82+
);
83+
});
84+
85+
it('should handle MoyasarError and call onPaymentResult with the error', async () => {
86+
const mockError = new GeneralError('Test Error');
87+
88+
(createPayment as jest.Mock).mockRejectedValue(mockError);
89+
90+
await onSamsungPayResponse(
91+
mockSamsungPayToken,
92+
mockOrderNumber,
93+
paymentConfigWithoutSaveOnlyFixture,
94+
onPaymentResult
95+
);
96+
97+
expect(onPaymentResult).toHaveBeenCalledWith(mockError);
98+
});
99+
100+
it('should handle non-MoyasarError and call onPaymentResult with NetworkError', async () => {
101+
const mockError = new Error('Network Error');
102+
(createPayment as jest.Mock).mockRejectedValue(mockError);
103+
104+
await onSamsungPayResponse(
105+
mockSamsungPayToken,
106+
mockOrderNumber,
107+
paymentConfigWithoutSaveOnlyFixture,
108+
onPaymentResult
109+
);
110+
111+
expect(onPaymentResult).toHaveBeenCalledWith(expect.any(NetworkError));
112+
});
113+
});

src/models/api/sources/samsung_pay/samsung_pay_request_source.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,28 @@ export class SamsungPayRequestSource implements PaymentRequestSource {
55
type: PaymentType = PaymentType.samsungPay;
66
samsungPayToken: string;
77
manualPayment: string;
8+
saveCard?: boolean;
89

910
constructor({
1011
samsungPayToken,
1112
manualPayment = false,
13+
saveCard = false,
1214
}: {
1315
samsungPayToken: string;
1416
manualPayment?: boolean;
17+
saveCard?: boolean;
1518
}) {
1619
this.samsungPayToken = samsungPayToken;
1720
this.manualPayment = manualPayment ? 'true' : 'false';
21+
this.saveCard = saveCard;
1822
}
1923

2024
toJson(): Record<string, any> {
2125
return {
2226
type: this.type,
2327
token: this.samsungPayToken,
2428
manual: this.manualPayment,
29+
save_card: this.saveCard,
2530
};
2631
}
2732
}

src/models/api/sources/samsung_pay/samsung_pay_response_source.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,26 @@ export class SamsungPayPaymentResponseSource implements PaymentResponseSource {
77
gatewayId: string;
88
referenceNumber?: string;
99
message?: string;
10+
token?: string;
1011

1112
constructor({
1213
number,
1314
gatewayId,
1415
referenceNumber,
1516
message,
17+
token,
1618
}: {
1719
number: string;
1820
gatewayId: string;
1921
referenceNumber?: string;
2022
message?: string;
23+
token?: string;
2124
}) {
2225
this.number = number;
2326
this.gatewayId = gatewayId;
2427
this.referenceNumber = referenceNumber;
2528
this.message = message;
29+
this.token = token;
2630
}
2731

2832
static fromJson(json: Record<string, any>): SamsungPayPaymentResponseSource {
@@ -32,6 +36,7 @@ export class SamsungPayPaymentResponseSource implements PaymentResponseSource {
3236
gatewayId: json.gateway_id,
3337
referenceNumber: json.reference_number,
3438
message: json.message,
39+
token: json.token,
3540
});
3641
}
3742
}

src/models/samsung_pay_config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export class SamsungPayConfig {
66
merchantName: string;
77
orderNumber?: string | null;
88
manual: boolean;
9+
saveCard?: boolean;
910

1011
/**
1112
* Constructs a new SamsungPayConfig instance with the provided settings.
@@ -20,21 +21,25 @@ export class SamsungPayConfig {
2021
* - Recommended format: Use a unique, traceable ID that can be linked to your system
2122
* - Note: Make sure to regenerate a new order number for each transaction
2223
* @param manual - An option to enable the manual auth and capture.
24+
* @param saveCard - An option to save (tokenize) the card after a successful payment.
2325
*/
2426
constructor({
2527
serviceId,
2628
merchantName,
2729
orderNumber,
2830
manual = false,
31+
saveCard = false,
2932
}: {
3033
serviceId: string;
3134
merchantName: string;
3235
orderNumber?: string | null;
3336
manual?: boolean;
37+
saveCard?: boolean;
3438
}) {
3539
this.serviceId = serviceId;
3640
this.merchantName = merchantName;
3741
this.orderNumber = orderNumber;
3842
this.manual = manual;
43+
this.saveCard = saveCard;
3944
}
4045
}

src/views/samsung_pay/samsung_pay.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export async function onSamsungPayResponse(
2525
const source = new SamsungPayRequestSource({
2626
samsungPayToken: token,
2727
manualPayment: paymentConfig.samsungPay?.manual,
28+
saveCard: paymentConfig.samsungPay?.saveCard,
2829
});
2930

3031
const paymentRequest = new PaymentRequest({

0 commit comments

Comments
 (0)