mirror of
https://github.com/valitydev/checkout.git
synced 2024-11-06 02:25:18 +00:00
Add ApiError to fetch api. Add retry predicate to withRetry (#287)
This commit is contained in:
parent
f687f4d55d
commit
e9faabd525
@ -107,7 +107,7 @@ const provideInvoiceUnpaid = async (model: PaymentModelInvoice): Promise<Payment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
console.error('provideInvoiceUnpaid error:', extractError(exception));
|
console.error(`provideInvoiceUnpaid error: ${extractError(exception)}`);
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'paymentProcessFailed',
|
name: 'paymentProcessFailed',
|
||||||
|
@ -1,37 +1,48 @@
|
|||||||
import { extractError } from './extractError';
|
import { extractError } from './extractError';
|
||||||
|
import { ApiError } from './fetchApi';
|
||||||
|
|
||||||
describe('extractError', () => {
|
describe('extractError', () => {
|
||||||
it('should return the correct message for standard Error instances', () => {
|
it('should extract message from Error instance', () => {
|
||||||
const error = new Error('Test error message');
|
const testError = new Error('Test error message');
|
||||||
expect(extractError(error)).toBe('Error: Test error message');
|
expect(extractError(testError)).toEqual('Error: Test error message');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the message from a custom error object with details', () => {
|
it('should handle ApiError object with a message in details', () => {
|
||||||
const error = {
|
const testApiError: ApiError = {
|
||||||
details: {
|
status: 404,
|
||||||
message: 'Custom error message',
|
endpoint: 'https://api.example.com/data',
|
||||||
},
|
details: { message: 'Resource not found' },
|
||||||
};
|
};
|
||||||
expect(extractError(error)).toBe('Custom error message');
|
expect(extractError(testApiError)).toEqual('Error 404 at https://api.example.com/data: Resource not found');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return a default message for non-Error objects without a message', () => {
|
it('should stringify details if message is not a string in ApiError', () => {
|
||||||
const error = { someProperty: 'someValue' };
|
const testApiError: ApiError = {
|
||||||
expect(extractError(error)).toBe('An unexpected error occurred.');
|
status: 500,
|
||||||
|
endpoint: 'https://api.example.com/data',
|
||||||
|
details: { error: 'Internal Server Error', code: 500 },
|
||||||
|
};
|
||||||
|
expect(extractError(testApiError)).toEqual(
|
||||||
|
`Error 500 at https://api.example.com/data: {"error":"Internal Server Error","code":500}`,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle null errors gracefully', () => {
|
it('should return a default message for non-object errors', () => {
|
||||||
const error = null;
|
expect(extractError('String error')).toEqual('An unexpected error occurred.');
|
||||||
expect(extractError(error)).toBe('An unexpected error occurred.');
|
expect(extractError(1234)).toEqual('An unexpected error occurred.');
|
||||||
|
expect(extractError(null)).toEqual('An unexpected error occurred.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle undefined errors gracefully', () => {
|
it('should return a default message for empty objects', () => {
|
||||||
const error = undefined;
|
expect(extractError({})).toEqual('An unexpected error occurred.');
|
||||||
expect(extractError(error)).toBe('An unexpected error occurred.');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return a default message for errors without a recognizable structure', () => {
|
it('should handle ApiError without details gracefully', () => {
|
||||||
const error = 'Some string error';
|
const testApiError: ApiError = {
|
||||||
expect(extractError(error)).toBe('An unexpected error occurred.');
|
status: 403,
|
||||||
|
endpoint: 'https://api.example.com/data',
|
||||||
|
details: {},
|
||||||
|
};
|
||||||
|
expect(extractError(testApiError)).toEqual('Error 403 at https://api.example.com/data: {}');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,18 +1,27 @@
|
|||||||
|
import { ApiError } from './fetchApi';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts a human-readable error message.
|
* Extracts a human-readable error message.
|
||||||
*
|
*
|
||||||
* @param {Error | { details?: { message?: string } } | unknown} error - The error to extract the message from.
|
* @param {Error | ApiError | unknown} error - The error to extract the message from.
|
||||||
* @returns {string} The extracted error message.
|
* @returns {string} The extracted error message.
|
||||||
*/
|
*/
|
||||||
export const extractError = (error: Error | { details?: { message?: string } } | unknown): string => {
|
export const extractError = (error: Error | ApiError | unknown): string => {
|
||||||
|
// Handling standard Error instances
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
return `${error.name}: ${error.message}`;
|
return `${error.name}: ${error.message}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handling ApiError objects
|
||||||
if (typeof error === 'object' && error !== null) {
|
if (typeof error === 'object' && error !== null) {
|
||||||
const message = (error as { details?: { message?: string } }).details?.message;
|
const apiError = error as ApiError;
|
||||||
if (typeof message === 'string') {
|
if ('status' in apiError && 'endpoint' in apiError && 'details' in apiError) {
|
||||||
return message;
|
// Attempting to extract a meaningful message from the details object
|
||||||
|
const detailsMessage = apiError.details.message || JSON.stringify(apiError.details);
|
||||||
|
return `Error ${apiError.status} at ${apiError.endpoint}: ${detailsMessage}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default message for when the error cannot be identified
|
||||||
return 'An unexpected error occurred.';
|
return 'An unexpected error occurred.';
|
||||||
};
|
};
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import { fetchApi } from './fetchApi';
|
import { fetchApi } from './fetchApi';
|
||||||
|
|
||||||
|
// TypeScript type assertion for mocking
|
||||||
|
declare let global: { fetch: jest.Mock };
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
global.fetch = jest.fn();
|
global.fetch = jest.fn();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('fetchApi', () => {
|
describe('fetchApi', () => {
|
||||||
it('handles successful responses correctly', async () => {
|
it('handles successful responses correctly', async () => {
|
||||||
(global.fetch as jest.Mock).mockResolvedValueOnce({
|
global.fetch.mockResolvedValueOnce({
|
||||||
ok: true,
|
ok: true,
|
||||||
json: () => Promise.resolve({ data: 'success' }),
|
json: () => Promise.resolve({ data: 'success' }),
|
||||||
});
|
});
|
||||||
@ -16,30 +19,33 @@ describe('fetchApi', () => {
|
|||||||
expect(data).toEqual({ data: 'success' });
|
expect(data).toEqual({ data: 'success' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws a generic error for non-JSON error responses', async () => {
|
it('throws an ApiError object for non-JSON error responses', async () => {
|
||||||
(global.fetch as jest.Mock).mockResolvedValueOnce({
|
global.fetch.mockResolvedValueOnce({
|
||||||
ok: false,
|
ok: false,
|
||||||
status: 500,
|
status: 500,
|
||||||
// Simulating a failure in response.json() method
|
|
||||||
json: () => Promise.reject(new Error('Failed to parse JSON')),
|
json: () => Promise.reject(new Error('Failed to parse JSON')),
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(fetchApi('https://api.example.com', 'token123', 'POST', 'path', {})).rejects.toThrow(
|
await expect(fetchApi('https://api.example.com', 'token123', 'POST', 'path', {})).rejects.toEqual({
|
||||||
`Status: 500 Endpoint: https://api.example.com/path`,
|
status: 500,
|
||||||
);
|
endpoint: 'https://api.example.com/path',
|
||||||
|
details: {}, // Assuming failure to parse JSON results in an empty details object
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error with JSON error details for 400 responses', async () => {
|
it('throws an ApiError object with JSON error details for 400 responses', async () => {
|
||||||
const errorDetails = { error: 'Bad Request', message: 'Invalid parameters' };
|
const errorDetails = { error: 'Bad Request', message: 'Invalid parameters' };
|
||||||
|
|
||||||
(global.fetch as jest.Mock).mockResolvedValueOnce({
|
global.fetch.mockResolvedValueOnce({
|
||||||
ok: false,
|
ok: false,
|
||||||
status: 400,
|
status: 400,
|
||||||
json: () => Promise.resolve({ error: 'Bad Request', message: 'Invalid parameters' }),
|
json: () => Promise.resolve(errorDetails),
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(fetchApi('https://api.example.com', 'token123', 'POST', 'path', {})).rejects.toThrow(
|
await expect(fetchApi('https://api.example.com', 'token123', 'POST', 'path', {})).rejects.toEqual({
|
||||||
`Status: 400 Endpoint: https://api.example.com/path ${JSON.stringify(errorDetails)}`,
|
status: 400,
|
||||||
);
|
endpoint: 'https://api.example.com/path',
|
||||||
|
details: errorDetails,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
import { guid } from './guid';
|
import { guid } from './guid';
|
||||||
|
|
||||||
|
export type ApiError = {
|
||||||
|
status: number;
|
||||||
|
endpoint: string;
|
||||||
|
details: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
export async function fetchApi(
|
export async function fetchApi(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
@ -7,7 +13,8 @@ export async function fetchApi(
|
|||||||
path: string,
|
path: string,
|
||||||
body?: any,
|
body?: any,
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const response = await fetch(`${endpoint}/${path}`, {
|
const fullEndpoint = `${endpoint}/${path}`;
|
||||||
|
const response = await fetch(fullEndpoint, {
|
||||||
method,
|
method,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json;charset=utf-8',
|
'Content-Type': 'application/json;charset=utf-8',
|
||||||
@ -18,14 +25,17 @@ export async function fetchApi(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
let errorDetails = `Status: ${response.status} Endpoint: ${endpoint}/${path}`;
|
let errorDetails: Record<string, any> = {};
|
||||||
try {
|
try {
|
||||||
const errorBody = await response.json();
|
errorDetails = await response.json();
|
||||||
errorDetails += ` ${JSON.stringify(errorBody)}`;
|
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
// Ignore error
|
// If the response is not in JSON format or cannot be parsed, errorDetails will remain an empty object
|
||||||
}
|
}
|
||||||
throw new Error(errorDetails);
|
throw {
|
||||||
|
status: response.status,
|
||||||
|
endpoint: fullEndpoint,
|
||||||
|
details: errorDetails,
|
||||||
|
} as ApiError;
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
@ -1,42 +1,33 @@
|
|||||||
import { withRetry } from './withRetry';
|
import { withRetry } from './withRetry';
|
||||||
|
|
||||||
// Mock async function that simulates varying behavior
|
// Mock the delay function to avoid actual waiting time during tests
|
||||||
const createMockAsyncFunction = (shouldSucceedAfterAttempts: number, result: string, error: Error): jest.Mock => {
|
jest.mock('./delay', () => ({
|
||||||
let attempts = 0;
|
delay: jest.fn(() => Promise.resolve()),
|
||||||
return jest.fn(() => {
|
}));
|
||||||
attempts++;
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (attempts >= shouldSucceedAfterAttempts) {
|
|
||||||
resolve(result);
|
|
||||||
} else {
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('withRetry', () => {
|
describe('withRetry', () => {
|
||||||
test('should succeed on first attempt', async () => {
|
it('retries until success for recoverable errors', async () => {
|
||||||
const mockFn = createMockAsyncFunction(1, 'success', new Error('fail'));
|
const mockRecoverableErrorFn = jest
|
||||||
const retriedFn = withRetry(mockFn, 3, 10);
|
.fn()
|
||||||
|
.mockRejectedValueOnce(new Error('Fail')) // Fail the first time
|
||||||
|
.mockResolvedValueOnce('Recovered'); // Succeed the second time
|
||||||
|
const retriedFn = withRetry(mockRecoverableErrorFn, 2, 100);
|
||||||
|
|
||||||
await expect(retriedFn()).resolves.toBe('success');
|
await expect(retriedFn()).resolves.toEqual('Recovered');
|
||||||
expect(mockFn).toHaveBeenCalledTimes(1);
|
expect(mockRecoverableErrorFn).toHaveBeenCalledTimes(2); // The function should be retried once
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should fail after all retries', async () => {
|
it('throws immediately on last attempt without further delay', async () => {
|
||||||
const mockFn = createMockAsyncFunction(5, 'success', new Error('fail')); // Succeeds after 5 attempts, but we only retry 3 times
|
const mockFailFn = jest.fn().mockRejectedValue(new Error('Mock error'));
|
||||||
const retriedFn = withRetry(mockFn, 3, 10);
|
const retriedFn = withRetry(mockFailFn, 3, 1000); // Allow for retries
|
||||||
|
|
||||||
await expect(retriedFn()).rejects.toThrow('fail');
|
const startTime = Date.now();
|
||||||
expect(mockFn).toHaveBeenCalledTimes(3);
|
await expect(retriedFn()).rejects.toThrow('Mock error');
|
||||||
});
|
const endTime = Date.now();
|
||||||
|
|
||||||
test('should succeed after 2 retries', async () => {
|
// Adjusting expectation due to potential slight delays in promise rejection handling
|
||||||
const mockFn = createMockAsyncFunction(3, 'success', new Error('fail')); // Succeeds on the third attempt
|
// Ensure that the total execution time is significantly less than the cumulative delay that would have occurred
|
||||||
const retriedFn = withRetry(mockFn, 3, 10);
|
expect(endTime - startTime).toBeLessThan(1000); // Significantly less than if it had waited after the last retry
|
||||||
|
expect(mockFailFn).toHaveBeenCalledTimes(3); // Attempted thrice
|
||||||
await expect(retriedFn()).resolves.toBe('success');
|
|
||||||
expect(mockFn).toHaveBeenCalledTimes(3);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
import { delay } from './delay';
|
import { delay } from './delay';
|
||||||
|
|
||||||
/**
|
// Defining a type for the predicate function
|
||||||
* Creates a retry wrapper for any async function, allowing retries on failure.
|
type ShouldRetryPredicate = (error: any) => boolean;
|
||||||
*
|
|
||||||
* @param {Function} asyncFn - The asynchronous function to wrap with retry logic.
|
// Default predicate function that allows retry only if the error is an instance of Error
|
||||||
* @param {number} maxRetries - The maximum number of retries.
|
const defaultShouldRetryPredicate: ShouldRetryPredicate = (error: any) => error instanceof Error;
|
||||||
* @param {number} retryDelay - The initial delay between retries in milliseconds.
|
|
||||||
* @returns {Function} - A new function that wraps the original async function with retry logic.
|
|
||||||
*/
|
|
||||||
export const withRetry =
|
export const withRetry =
|
||||||
<T>(asyncFn: (...args: any[]) => Promise<T>, maxRetries = 3, retryDelay = 3000) =>
|
<T>(
|
||||||
|
asyncFn: (...args: any[]) => Promise<T>,
|
||||||
|
maxRetries = 3,
|
||||||
|
retryDelay = 3000,
|
||||||
|
shouldRetryPredicate: ShouldRetryPredicate = defaultShouldRetryPredicate,
|
||||||
|
) =>
|
||||||
async (...args: any[]): Promise<T> => {
|
async (...args: any[]): Promise<T> => {
|
||||||
let lastError: any;
|
let lastError: any;
|
||||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||||
@ -17,12 +20,12 @@ export const withRetry =
|
|||||||
return await asyncFn(...args);
|
return await asyncFn(...args);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastError = error;
|
lastError = error;
|
||||||
// If this is the last attempt, throw the error immediately
|
// If this is the last attempt or the predicate returns false, throw the error immediately
|
||||||
if (attempt === maxRetries - 1) {
|
if (attempt === maxRetries - 1 || !shouldRetryPredicate(error)) {
|
||||||
throw lastError;
|
throw lastError;
|
||||||
}
|
}
|
||||||
await delay(retryDelay);
|
await delay(retryDelay);
|
||||||
// Increase retryDelay for exponential backoff
|
// Optional: Increase retryDelay for exponential backoff
|
||||||
retryDelay *= 2;
|
retryDelay *= 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ export const usePaymentCondition = (model: PaymentModel, initConditions: Payment
|
|||||||
apiMethodCall: 1000,
|
apiMethodCall: 1000,
|
||||||
});
|
});
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
console.error(`startPayment error: ${extractError(exception)}`, exception);
|
console.error(`startPayment error: ${extractError(exception)}`);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'COMBINE_CONDITIONS',
|
type: 'COMBINE_CONDITIONS',
|
||||||
payload: [
|
payload: [
|
||||||
@ -65,7 +65,7 @@ export const usePaymentCondition = (model: PaymentModel, initConditions: Payment
|
|||||||
apiMethodCall: 3000,
|
apiMethodCall: 3000,
|
||||||
});
|
});
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
console.error(`startWaitingPaymentResult error: ${extractError(exception)}`, exception);
|
console.error(`startWaitingPaymentResult error: ${extractError(exception)}`);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'COMBINE_CONDITIONS',
|
type: 'COMBINE_CONDITIONS',
|
||||||
payload: [
|
payload: [
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import { useContext, useEffect, useMemo, useState } from 'react';
|
import { useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { Gateway } from 'checkout/backend/p2p';
|
||||||
|
import { LocaleContext, PaymentConditionsContext, PaymentContext, PaymentModelContext } from 'checkout/contexts';
|
||||||
|
import { InvoiceDetermined, PaymentStarted } from 'checkout/paymentCondition';
|
||||||
|
import { isNil } from 'checkout/utils';
|
||||||
|
|
||||||
import { CompletePayment } from './CompletePayment';
|
import { CompletePayment } from './CompletePayment';
|
||||||
import { Destinations } from './Destinations';
|
import { Destinations } from './Destinations';
|
||||||
import { GatewaySelector } from './GatewaySelector';
|
import { GatewaySelector } from './GatewaySelector';
|
||||||
import { Gateway } from '../../../common/backend/p2p';
|
|
||||||
import { LocaleContext, PaymentConditionsContext, PaymentContext, PaymentModelContext } from '../../../common/contexts';
|
|
||||||
import { InvoiceDetermined, PaymentStarted } from '../../../common/paymentCondition';
|
|
||||||
import { isNil } from '../../../common/utils';
|
|
||||||
import { FormLoader } from '../../../components/legacy';
|
import { FormLoader } from '../../../components/legacy';
|
||||||
|
|
||||||
const FormContainer = styled.div`
|
const FormContainer = styled.div`
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { Locale } from 'checkout/contexts';
|
||||||
|
import { isNil } from 'checkout/utils';
|
||||||
|
|
||||||
import { Info } from './commonComponents';
|
import { Info } from './commonComponents';
|
||||||
import { useComplete } from './useComplete';
|
import { useComplete } from './useComplete';
|
||||||
import { Locale } from '../../../common/contexts';
|
|
||||||
import { isNil } from '../../../common/utils';
|
|
||||||
import { Button } from '../../../components/legacy';
|
import { Button } from '../../../components/legacy';
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
|
|
||||||
|
import { Destination } from 'checkout/backend/p2p';
|
||||||
|
import { Locale } from 'checkout/contexts';
|
||||||
|
import { ViewModelContext } from 'checkout/contexts';
|
||||||
|
|
||||||
import { DestinationInfoBankAccount } from './DestinationInfoBankAccount';
|
import { DestinationInfoBankAccount } from './DestinationInfoBankAccount';
|
||||||
import { DestinationInfoBankCard } from './DestinationInfoBankCard';
|
import { DestinationInfoBankCard } from './DestinationInfoBankCard';
|
||||||
import { DestinationInfoSpb } from './DestinationInfoSpb';
|
import { DestinationInfoSpb } from './DestinationInfoSpb';
|
||||||
import { Destination } from '../../../../common/backend/p2p';
|
|
||||||
import { Locale } from '../../../../common/contexts';
|
|
||||||
import { ViewModelContext } from '../../../../common/contexts';
|
|
||||||
import { Info, Container, Row, Label, Value, Alert } from '../commonComponents';
|
import { Info, Container, Row, Label, Value, Alert } from '../commonComponents';
|
||||||
|
|
||||||
type DestinationInfoProps = {
|
type DestinationInfoProps = {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
import { DestinationBankAccount } from 'checkout/backend/p2p';
|
||||||
|
import { Locale } from 'checkout/contexts';
|
||||||
|
|
||||||
import { CopyToClipboard } from './CopyToClipboard';
|
import { CopyToClipboard } from './CopyToClipboard';
|
||||||
import { DestinationBankAccount } from '../../../../common/backend/p2p';
|
|
||||||
import { Locale } from '../../../../common/contexts';
|
|
||||||
import { Container, Label, Row, Value } from '../commonComponents';
|
import { Container, Label, Row, Value } from '../commonComponents';
|
||||||
|
|
||||||
export type DestinationInfoBankCardInfo = {
|
export type DestinationInfoBankCardInfo = {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
import { DestinationBankCard } from 'checkout/backend/p2p';
|
||||||
|
import { Locale } from 'checkout/contexts';
|
||||||
|
|
||||||
import { CopyToClipboard } from './CopyToClipboard';
|
import { CopyToClipboard } from './CopyToClipboard';
|
||||||
import { DestinationBankCard } from '../../../../common/backend/p2p';
|
|
||||||
import { Locale } from '../../../../common/contexts';
|
|
||||||
import { Container, Label, Row, Value } from '../commonComponents';
|
import { Container, Label, Row, Value } from '../commonComponents';
|
||||||
|
|
||||||
export type DestinationInfoBankCardInfo = {
|
export type DestinationInfoBankCardInfo = {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
import { DestinationSBP } from 'checkout/backend/p2p';
|
||||||
|
import { Locale } from 'checkout/contexts';
|
||||||
|
|
||||||
import { CopyToClipboard } from './CopyToClipboard';
|
import { CopyToClipboard } from './CopyToClipboard';
|
||||||
import { DestinationSBP } from '../../../../common/backend/p2p';
|
|
||||||
import { Locale } from '../../../../common/contexts';
|
|
||||||
import { Container, Label, Row, Value } from '../commonComponents';
|
import { Container, Label, Row, Value } from '../commonComponents';
|
||||||
|
|
||||||
export type DestinationInfoSpbProps = {
|
export type DestinationInfoSpbProps = {
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { Locale } from 'checkout/contexts';
|
||||||
|
import { isNil } from 'checkout/utils';
|
||||||
|
|
||||||
import { DestinationInfo } from './DestinationInfo';
|
import { DestinationInfo } from './DestinationInfo';
|
||||||
import { useDestinations } from './useDestinations';
|
import { useDestinations } from './useDestinations';
|
||||||
import { Locale } from '../../../common/contexts';
|
|
||||||
import { isNil } from '../../../common/utils';
|
|
||||||
|
|
||||||
export type DestinationsProps = {
|
export type DestinationsProps = {
|
||||||
locale: Locale;
|
locale: Locale;
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { Gateway } from 'checkout/backend/p2p';
|
||||||
|
import { Locale } from 'checkout/contexts';
|
||||||
|
|
||||||
import { useGateways } from './useGateways';
|
import { useGateways } from './useGateways';
|
||||||
import { Gateway } from '../../../common/backend/p2p';
|
|
||||||
import { Locale } from '../../../common/contexts';
|
|
||||||
import { Select } from '../../../components/legacy';
|
import { Select } from '../../../components/legacy';
|
||||||
|
|
||||||
export type GatewaySelectorProps = {
|
export type GatewaySelectorProps = {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useReducer } from 'react';
|
import { useCallback, useReducer } from 'react';
|
||||||
|
|
||||||
import { complete as completeApi } from '../../../common/backend/p2p';
|
import { complete as completeApi } from 'checkout/backend/p2p';
|
||||||
import { extractError, withRetry } from '../../../common/utils';
|
import { extractError, withRetry } from 'checkout/utils';
|
||||||
|
|
||||||
type State = { status: 'PRISTINE' | 'LOADING' | 'SUCCESS' | 'FAILURE' };
|
type State = { status: 'PRISTINE' | 'LOADING' | 'SUCCESS' | 'FAILURE' };
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useReducer, useRef } from 'react';
|
import { useCallback, useReducer, useRef } from 'react';
|
||||||
|
|
||||||
import { Destination, getDestinations as getApiDestinations } from '../../../common/backend/p2p';
|
import { Destination, getDestinations as getApiDestinations } from 'checkout/backend/p2p';
|
||||||
import { extractError, withRetry } from '../../../common/utils';
|
import { extractError, withRetry } from 'checkout/utils';
|
||||||
|
|
||||||
type State = { status: 'PRISTINE' | 'LOADING' | 'FAILURE' } | { status: 'SUCCESS'; data: Destination[] };
|
type State = { status: 'PRISTINE' | 'LOADING' | 'FAILURE' } | { status: 'SUCCESS'; data: Destination[] };
|
||||||
|
|
||||||
@ -52,7 +52,10 @@ export const useDestinations = (
|
|||||||
dispatch({ type: 'FETCH_SUCCESS', payload: destinations });
|
dispatch({ type: 'FETCH_SUCCESS', payload: destinations });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch({ type: 'FETCH_FAILURE' });
|
dispatch({ type: 'FETCH_FAILURE' });
|
||||||
console.error(`Failed to fetch destinations. ${extractError(error)}`);
|
// Api returns 500 error when there are no no requisites available.
|
||||||
|
if ('status' in error && error.status !== 500) {
|
||||||
|
console.error(`Failed to fetch destinations. ${extractError(error)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [capiEndpoint, accessToken, invoiceID, paymentID, gatewayID]);
|
}, [capiEndpoint, accessToken, invoiceID, paymentID, gatewayID]);
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useReducer } from 'react';
|
import { useCallback, useReducer } from 'react';
|
||||||
|
|
||||||
import { Gateway, getGateways as getApiGateways } from '../../../common/backend/p2p';
|
import { Gateway, getGateways as getApiGateways } from 'checkout/backend/p2p';
|
||||||
import { extractError, withRetry } from '../../../common/utils';
|
import { extractError, withRetry } from 'checkout/utils';
|
||||||
|
|
||||||
type State = { status: 'PRISTINE' | 'LOADING' | 'FAILURE' } | { status: 'SUCCESS'; data: Gateway[] };
|
type State = { status: 'PRISTINE' | 'LOADING' | 'FAILURE' } | { status: 'SUCCESS'; data: Gateway[] };
|
||||||
|
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import { useContext, useEffect } from 'react';
|
import { useContext, useEffect } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import { IconName } from './types';
|
|
||||||
import { getResultInfo } from './utils';
|
|
||||||
import {
|
import {
|
||||||
CompletePaymentContext,
|
CompletePaymentContext,
|
||||||
LocaleContext,
|
LocaleContext,
|
||||||
PaymentConditionsContext,
|
PaymentConditionsContext,
|
||||||
PaymentModelContext,
|
PaymentModelContext,
|
||||||
} from '../../../common/contexts';
|
} from 'checkout/contexts';
|
||||||
import { isNil, last } from '../../../common/utils';
|
import { isNil, last } from 'checkout/utils';
|
||||||
|
|
||||||
|
import { IconName } from './types';
|
||||||
|
import { getResultInfo } from './utils';
|
||||||
import { Button, ErrorIcon, Link, SuccessIcon, WarningIcon } from '../../../components/legacy';
|
import { Button, ErrorIcon, Link, SuccessIcon, WarningIcon } from '../../../components/legacy';
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
|
@ -16,7 +16,10 @@
|
|||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"baseUrl": "."
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"checkout/*": ["./src/common/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["./src/**/*", "./types/**/*"]
|
"include": ["./src/**/*", "./types/**/*"]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user