Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions apps/payments/next/.env
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,10 @@ PAYMENTS_NEXT_HOSTED_URL=http://localhost:3035
SUBSCRIPTIONS_UNSUPPORTED_LOCATIONS='["CN", "KP", "IR", "SY", "CU", "SD", "BY", "IQ", "OM", "RU", "TR", "TM", "AE"]'

SP2MAP__OFFERINGS={"123donepro":{"USD":{"monthly":"prod_GqM9ToKK62qjkK,plan_GqM9N6qyhvxaVk","halfyearly":"prod_GqM9ToKK62qjkK,price_1LTAC5BVqmGyQTManGVoSBsc","yearly":"prod_GqM9ToKK62qjkK,price_1KbomlBVqmGyQTMaa0Tq7UaW"},"EUR":{"monthly":"prod_GqM9ToKK62qjkK,price_1H8NnnBVqmGyQTMaLwLRKbF3"}},"123doneproplus":{"USD":{"monthly":"prod_GyHm8uwOIjr6k5,price_1NsBeHBVqmGyQTMa0o3zMSH3"}},"123foxkeh":{"USD":{"monthly":"prod_OfV6ko0QPHotas,price_1NsA5qBVqmGyQTMapXvSdxYC"}},"foxkeh":{"USD":{"daily":"prod_GvH2k78kKusAlV,price_1Pe1GiBVqmGyQTMaaPVElv5S","monthly":"prod_GvH2k78kKusAlV,price_1LxakKBVqmGyQTMas2fZaSCG"}},"foxkeh2":{"USD":{"monthly":"prod_OfWo3Xmsn2dOpA,price_1NsBknBVqmGyQTMaXvfEARm2"}},"vpn":{"USD":{"monthly":"prod_JYy0wNbTbA5fDv,price_1Ivq4gBVqmGyQTMaplHcFEGO"}}}

SP2REDIRECT__ENABLED=true
SP2REDIRECT__SHADOW_MODE=true
SP2REDIRECT__DEFAULT_REDIRECT_PERCENTAGE=100
SP2REDIRECT__OFFERINGS={"123donepro":100,"123doneproplus":100,"123foxkeh":100,"foxkeh":100,"foxkeh2":100,"vpn":100}

# Nextjs Public Environment Variables
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,23 @@ import {
import { BaseParams, buildRedirectUrl } from '@fxa/payments/ui';
import { config } from 'apps/payments/next/config';
import { getIpAddress } from '@fxa/payments/ui/server';
import { getSP2Params } from '@fxa/payments/legacy';
import {
buildSp2RedirectUrl,
getSP2Params,
redirectToSp2,
} from '@fxa/payments/legacy';
import crypto from 'crypto';
import * as Sentry from '@sentry/nextjs';

export const dynamic = 'force-dynamic'; // defaults to auto

function reportError(message: string, details?: any) {
if (details) {
console.error(message, details);
Sentry.captureMessage(message, details);
} else {
console.error(message);
Sentry.captureMessage(message, 'error');
}
}

Expand All @@ -45,17 +53,47 @@ export async function GET(
request: NextRequest,
{ params }: { params: BaseParams }
) {
const currency = await determineCurrencyAction(getIpAddress());
const { productId, priceId } = getSP2Params(
config.sp2map,
reportError,
params.offeringId,
params.interval,
currency
);
console.log({ productId, priceId });
const requestSearchParams = request.nextUrl.searchParams;

if (config.sp2redirect.enabled) {
const queryCurrency = requestSearchParams.get('currency');
const querySpVersion = requestSearchParams.get('spVersion');
const isSp2Redirect = redirectToSp2(
config.sp2redirect,
params.offeringId,
crypto.randomInt(1, 100),
reportError
);

if (isSp2Redirect || querySpVersion === '2') {
const currency = queryCurrency
? queryCurrency
: await determineCurrencyAction(getIpAddress());
const { productId, priceId } = getSP2Params(
config.sp2map,
reportError,
params.offeringId,
params.interval,
currency
);

const sp2RedirectUrl = buildSp2RedirectUrl(
productId,
priceId,
config.contentServerUrl,
requestSearchParams
);

if (!config.sp2redirect.shadowMode) {
redirect(sp2RedirectUrl);
} else {
console.log('SP2 Redirect Shadow Mode enabled', { sp2RedirectUrl });
}
}
}

const searchParams = Object.fromEntries(requestSearchParams);

const searchParams = Object.fromEntries(request.nextUrl.searchParams);
const redirectToUrl = new URL(
buildRedirectUrl(params.offeringId, params.interval, 'new', 'checkout', {
locale: params.locale,
Expand Down
7 changes: 6 additions & 1 deletion apps/payments/next/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
RootConfig as NestAppRootConfig,
validate,
} from '@fxa/payments/ui/server';
import { SP2MapConfig } from '@fxa/payments/legacy';
import { SP2MapConfig, SP2RedirectConfig } from '@fxa/payments/legacy';

class CspConfig {
@IsUrl()
Expand Down Expand Up @@ -104,6 +104,11 @@ export class PaymentsNextConfig extends NestAppRootConfig {
@IsDefined()
sp2map!: SP2MapConfig;

@Type(() => SP2RedirectConfig)
@ValidateNested()
@IsDefined()
sp2redirect!: SP2RedirectConfig;

@IsString()
authSecret!: string;

Expand Down
2 changes: 2 additions & 0 deletions libs/payments/legacy/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './lib/stripe-mapper.service';
export * from './lib/sp2map.config';
export * from './lib/utils/buildSp2RedirectUrl';
export * from './lib/utils/getSP2Params';
export * from './lib/utils/redirectToSp2';
28 changes: 27 additions & 1 deletion libs/payments/legacy/src/lib/factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { SP2MapConfig, Currency, Intervals } from './sp2map.config';
import { faker } from '@faker-js/faker';
import {
SP2MapConfig,
Currency,
Intervals,
SP2RedirectConfig,
RedirectParams,
} from './sp2map.config';
import { StripeMetadataWithCMS } from './types';

export const StripeMetadataWithCMSFactory = (
Expand Down Expand Up @@ -31,3 +38,22 @@ export const IntervalsFactory = (override?: Partial<Intervals>): Intervals => ({
monthly: ['prod_productid', 'price_priceId'],
...override,
});

export const RedirectParamsFactory = (
override?: Partial<RedirectParams>
): RedirectParams => ({
sp2RedirectPercentage: faker.number.int({ min: 0, max: 100 }),
...override,
});

export const SP2RedirectConfigFactory = (
override?: Partial<SP2RedirectConfig>
): SP2RedirectConfig => ({
enabled: true,
shadowMode: false,
defaultRedirectPercentage: 100,
offerings: {
vpn: RedirectParamsFactory(),
},
...override,
});
60 changes: 59 additions & 1 deletion libs/payments/legacy/src/lib/sp2map.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { plainToInstance, Transform } from 'class-transformer';
import { plainToInstance, Transform, Type } from 'class-transformer';
import {
IsString,
IsDefined,
IsObject,
IsOptional,
validateSync,
IsBoolean,
IsNumber,
Min,
Max,
} from 'class-validator';

export class Intervals {
Expand Down Expand Up @@ -89,3 +93,57 @@ export class SP2MapConfig {
})
offerings!: Record<string, Currency | undefined>; // Index signature to allow dynamic keys
}

export class RedirectParams {
@IsDefined()
@IsNumber()
@Min(0)
@Max(100)
@Type(() => Number)
sp2RedirectPercentage!: number;
}

export class SP2RedirectConfig {
@IsDefined()
@IsBoolean()
enabled!: boolean;

@IsDefined()
@IsBoolean()
shadowMode!: boolean;

@IsDefined()
@IsNumber()
@Min(0)
@Max(100)
@Type(() => Number)
defaultRedirectPercentage!: number;

@IsDefined()
@IsObject()
@Transform(({ value }) => {
const parsedValue = JSON.parse(value);
const transformedValue = Object.entries(parsedValue).reduce(
(acc, [key, val]) => {
const classVal = plainToInstance(RedirectParams, {
sp2RedirectPercentage: val,
});
try {
const validation = validateSync(classVal);
if (validation.length > 0) {
throw new Error(`Validation errors: ${JSON.stringify(validation)}`);
}
} catch (err) {
throw new Error(
`Validation issue with value: ${JSON.stringify(val)}`
);
}
acc[key] = classVal;
return acc;
},
{} as any
);
return transformedValue;
})
offerings!: Record<string, RedirectParams | undefined>;
}
40 changes: 40 additions & 0 deletions libs/payments/legacy/src/lib/utils/buildSp2RedirectUrl.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { buildSp2RedirectUrl } from './buildSp2RedirectUrl';

describe('buildSp2RedirectUrl', () => {
const defaultProductId = 'prod_123';
const defaultPriceId = 'price_123';
const defaultContentServerUrl = 'http://content-server.com';
const defaultSearchParams = new URLSearchParams(
'flow_id=one&flow_begin_time=123'
);

it('should return the correct URL', () => {
const result = buildSp2RedirectUrl(
defaultProductId,
defaultPriceId,
defaultContentServerUrl,
defaultSearchParams
);
expect(result).toBe(
'http://content-server.com/subscriptions/products/prod_123?plan=price_123&flow_id=one&flow_begin_time=123'
);
});

it('should remove SP2 redirect logic specific query params', () => {
defaultSearchParams.append('currency', 'USD');
defaultSearchParams.append('spVersion', '2');
const result = buildSp2RedirectUrl(
defaultProductId,
defaultPriceId,
defaultContentServerUrl,
defaultSearchParams
);
expect(result).toBe(
'http://content-server.com/subscriptions/products/prod_123?plan=price_123&flow_id=one&flow_begin_time=123'
);
});
});
18 changes: 18 additions & 0 deletions libs/payments/legacy/src/lib/utils/buildSp2RedirectUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

export function buildSp2RedirectUrl(
productId: string,
priceId: string,
contentServerUrl: string,
searchParams: URLSearchParams
) {
// Remove SP2 redirect logic specific query params
searchParams.delete('currency');
searchParams.delete('spVersion');

const remainingQueryParams = searchParams.toString();

return `${contentServerUrl}/subscriptions/products/${productId}?plan=${priceId}&${remainingQueryParams}`;
}
Loading