diff --git a/bats/core/api/referral.bats b/bats/core/api/referral.bats new file mode 100644 index 00000000000..f6ced1d6853 --- /dev/null +++ b/bats/core/api/referral.bats @@ -0,0 +1,9 @@ +#!/usr/bin/env bats +load "../../helpers/user.bash" + +@test "referral: alice is using bob referral" { + local token_name=$1 + local phone=$(random_phone) + + login_user "$token_name" "$phone" "bob" +} diff --git a/bats/helpers/user.bash b/bats/helpers/user.bash index e680ed4f3f2..a96025b9310 100644 --- a/bats/helpers/user.bash +++ b/bats/helpers/user.bash @@ -5,6 +5,7 @@ source "$(dirname "$CURRENT_FILE")/cli.bash" login_user() { local token_name=$1 local phone=$2 + local referral=$3 local code="000000" @@ -13,7 +14,8 @@ login_user() { jq -n \ --arg phone "$phone" \ --arg code "$code" \ - '{input: {phone: $phone, code: $code}}' + --arg referral "$referral" \ + '{input: {phone: $phone, code: $code, referral: $referral}}' ) exec_graphql 'anon' 'user-login' "$variables" auth_token="$(graphql_output '.data.userLogin.authToken')" diff --git a/core/api/Makefile b/core/api/Makefile index 0bc0fe3265c..640380c5f42 100644 --- a/core/api/Makefile +++ b/core/api/Makefile @@ -135,8 +135,5 @@ redis-cli: redis-flush: docker-compose exec redis redis-cli FLUSHDB -codegen: - pnpm run write-sdl - gen-test-jwt: pnpm run gen-test-jwt diff --git a/core/api/src/app/authentication/login.ts b/core/api/src/app/authentication/login.ts index d6ddfd8587f..8ea171c848e 100644 --- a/core/api/src/app/authentication/login.ts +++ b/core/api/src/app/authentication/login.ts @@ -62,10 +62,12 @@ export const loginWithPhoneToken = async ({ phone, code, ip, + referral, }: { phone: PhoneNumber code: PhoneCode ip: IpAddress + referral: Referral | undefined }): Promise => { { const limitOk = await checkFailedLoginAttemptPerIpLimits(ip) @@ -103,6 +105,7 @@ export const loginWithPhoneToken = async ({ const kratosResult = await authService.createIdentityWithSession({ phone, phoneMetadata, + referral, }) if (kratosResult instanceof Error) return kratosResult diff --git a/core/api/src/domain/authentication/index.types.d.ts b/core/api/src/domain/authentication/index.types.d.ts index f229cbfcc18..b4d0cf1e874 100644 --- a/core/api/src/domain/authentication/index.types.d.ts +++ b/core/api/src/domain/authentication/index.types.d.ts @@ -83,6 +83,7 @@ interface IAuthWithPhonePasswordlessService { createIdentityWithSession(args: { phone: PhoneNumber phoneMetadata?: PhoneMetadata + referral: Referral | undefined }): Promise updateIdentityFromDeviceAccount(args: { phone: PhoneNumber diff --git a/core/api/src/domain/errors.ts b/core/api/src/domain/errors.ts index ba4c4dcda8e..83c97ca1906 100644 --- a/core/api/src/domain/errors.ts +++ b/core/api/src/domain/errors.ts @@ -26,7 +26,7 @@ export class CannotConnectToDbError extends RepositoryError { export class DbConnectionClosedError extends RepositoryError { level = ErrorLevel.Critical } -export class MultipleWalletsFoundForAccountIdAndCurrency extends RepositoryError {} +export class MultipleWalletsFoundForAccountIdAndCurrencyError extends RepositoryError {} export class WalletInvoiceMissingLnInvoiceError extends RepositoryError {} export class CouldNotUnsetPhoneFromUserError extends CouldNotUpdateError {} @@ -87,6 +87,7 @@ export class InvalidPubKeyError extends ValidationError {} export class InvalidScanDepthAmount extends ValidationError {} export class SatoshiAmountRequiredError extends ValidationError {} export class InvalidUsername extends ValidationError {} +export class InvalidReferralError extends ValidationError {} export class InvalidDeviceId extends ValidationError {} export class InvalidIdentityPassword extends ValidationError {} export class InvalidIdentityUsername extends ValidationError {} diff --git a/core/api/src/domain/users/index.ts b/core/api/src/domain/users/index.ts index 1c6fb89e899..b2135d5ccc4 100644 --- a/core/api/src/domain/users/index.ts +++ b/core/api/src/domain/users/index.ts @@ -8,6 +8,7 @@ import { InvalidIdentityUsername, InvalidLanguageError, InvalidPhoneNumber, + InvalidReferralError, } from "@/domain/errors" export * from "./phone-metadata-authorizer" @@ -89,4 +90,11 @@ export const checkedToIdentityPassword = ( return password as IdentityPassword } +export const checkedToReferral = (referral: string): Referral | ValidationError => { + if (referral.length > 12) { + return new InvalidReferralError(referral) + } + return referral as Referral +} + export { Languages } diff --git a/core/api/src/domain/users/index.types.d.ts b/core/api/src/domain/users/index.types.d.ts index 660bf6ac419..a71ddb299c2 100644 --- a/core/api/src/domain/users/index.types.d.ts +++ b/core/api/src/domain/users/index.types.d.ts @@ -1,6 +1,8 @@ type PhoneNumber = string & { readonly brand: unique symbol } type PhoneCode = string & { readonly brand: unique symbol } +type Referral = string & { readonly brand: unique symbol } + type EmailAddress = string & { readonly brand: unique symbol } type EmailCode = string & { readonly brand: unique symbol } diff --git a/core/api/src/graphql/error-map.ts b/core/api/src/graphql/error-map.ts index 7b530e5c461..70f95110951 100644 --- a/core/api/src/graphql/error-map.ts +++ b/core/api/src/graphql/error-map.ts @@ -676,7 +676,8 @@ export const mapError = (error: ApplicationError): CustomGraphQLError => { case "InvalidErrorCodeForPhoneMetadataError": case "InvalidMobileCountryCodeForPhoneMetadataError": case "InvalidCountryCodeForPhoneMetadataError": - case "MultipleWalletsFoundForAccountIdAndCurrency": + case "MultipleWalletsFoundForAccountIdAndCurrencyError": + case "InvalidReferralError": message = `Unexpected error occurred, please try again or contact support if it persists (code: ${ error.name }${error.message ? ": " + error.message : ""})` diff --git a/core/api/src/graphql/public/root/mutation/user-login.ts b/core/api/src/graphql/public/root/mutation/user-login.ts index 9ce119fca80..5dbdd37e68a 100644 --- a/core/api/src/graphql/public/root/mutation/user-login.ts +++ b/core/api/src/graphql/public/root/mutation/user-login.ts @@ -5,6 +5,7 @@ import Phone from "@/graphql/shared/types/scalar/phone" import AuthTokenPayload from "@/graphql/shared/types/payload/auth-token" import { mapAndParseErrorForGqlResponse } from "@/graphql/error-map" import { Authentication } from "@/app" +import Referral from "@/graphql/shared/types/scalar/referral" const UserLoginInput = GT.Input({ name: "UserLoginInput", @@ -15,6 +16,9 @@ const UserLoginInput = GT.Input({ code: { type: GT.NonNull(OneTimeAuthCode), }, + referral: { + type: Referral, + }, }), }) @@ -25,6 +29,7 @@ const UserLoginMutation = GT.Field< input: { phone: PhoneNumber | InputValidationError code: PhoneCode | InputValidationError + referral?: Referral | InputValidationError } } >({ @@ -36,7 +41,7 @@ const UserLoginMutation = GT.Field< input: { type: GT.NonNull(UserLoginInput) }, }, resolve: async (_, args, { ip }) => { - const { phone, code } = args.input + const { phone, code, referral } = args.input if (phone instanceof Error) { return { errors: [{ message: phone.message }] } @@ -46,6 +51,10 @@ const UserLoginMutation = GT.Field< return { errors: [{ message: code.message }] } } + if (referral instanceof Error) { + return { errors: [{ message: referral.message }] } + } + if (ip === undefined) { return { errors: [{ message: "ip is undefined" }] } } @@ -54,6 +63,7 @@ const UserLoginMutation = GT.Field< phone, code, ip, + referral, }) if (res instanceof Error) { diff --git a/core/api/src/graphql/shared/types/scalar/referral.ts b/core/api/src/graphql/shared/types/scalar/referral.ts new file mode 100644 index 00000000000..fd0b72b80a8 --- /dev/null +++ b/core/api/src/graphql/shared/types/scalar/referral.ts @@ -0,0 +1,31 @@ +import { checkedToReferral } from "@/domain/users" +import { InputValidationError } from "@/graphql/error" +import { GT } from "@/graphql/index" + +const Referral = GT.Scalar({ + name: "Referral", + description: "Referral code provided by a community", + parseValue(value) { + if (typeof value !== "string") { + return new InputValidationError({ message: "Invalid type for Referral" }) + } + return validReferralValue(value) + }, + parseLiteral(ast) { + if (ast.kind === GT.Kind.STRING) { + return validReferralValue(ast.value) + } + return new InputValidationError({ message: "Invalid type for Referral" }) + }, +}) + +function validReferralValue(value: string) { + const ReferralNumberValid = checkedToReferral(value) + if (ReferralNumberValid instanceof Error) + return new InputValidationError({ + message: "Referral is not a valid", + }) + return ReferralNumberValid +} + +export default Referral diff --git a/core/api/src/services/mongoose/wallets.ts b/core/api/src/services/mongoose/wallets.ts index 1cd9a059002..4d471e9992a 100644 --- a/core/api/src/services/mongoose/wallets.ts +++ b/core/api/src/services/mongoose/wallets.ts @@ -13,7 +13,7 @@ import { CouldNotFindWalletFromOnChainAddressesError, CouldNotListWalletsFromAccountIdError, CouldNotListWalletsFromWalletCurrencyError, - MultipleWalletsFoundForAccountIdAndCurrency, + MultipleWalletsFoundForAccountIdAndCurrencyError, } from "@/domain/errors" export const WalletsRepository = (): IWalletsRepository => { @@ -99,7 +99,7 @@ export const WalletsRepository = (): IWalletsRepository => { return new CouldNotFindWalletFromAccountIdAndCurrencyError(WalletCurrency.Btc) } if (btcWallets.length > 1) { - return new MultipleWalletsFoundForAccountIdAndCurrency(WalletCurrency.Btc) + return new MultipleWalletsFoundForAccountIdAndCurrencyError(WalletCurrency.Btc) } const btcWallet = btcWallets[0] @@ -108,7 +108,7 @@ export const WalletsRepository = (): IWalletsRepository => { return new CouldNotFindWalletFromAccountIdAndCurrencyError(WalletCurrency.Usd) } if (usdWallets.length > 1) { - return new MultipleWalletsFoundForAccountIdAndCurrency(WalletCurrency.Usd) + return new MultipleWalletsFoundForAccountIdAndCurrencyError(WalletCurrency.Usd) } const usdWallet = usdWallets[0] diff --git a/core/api/test/integration/services/wallets-repository.spec.ts b/core/api/test/integration/services/wallets-repository.spec.ts index e3ab0873239..072e81651e0 100644 --- a/core/api/test/integration/services/wallets-repository.spec.ts +++ b/core/api/test/integration/services/wallets-repository.spec.ts @@ -2,7 +2,7 @@ import { randomUUID } from "crypto" import { CouldNotFindWalletFromAccountIdAndCurrencyError, - MultipleWalletsFoundForAccountIdAndCurrency, + MultipleWalletsFoundForAccountIdAndCurrencyError, RepositoryError, } from "@/domain/errors" import { WalletCurrency } from "@/domain/shared" @@ -68,7 +68,9 @@ describe("WalletsRepository", () => { await newWallet(WalletCurrency.Usd) const accountWallets = await wallets.findAccountWalletsByAccountId(accountId) - expect(accountWallets).toBeInstanceOf(MultipleWalletsFoundForAccountIdAndCurrency) + expect(accountWallets).toBeInstanceOf( + MultipleWalletsFoundForAccountIdAndCurrencyError, + ) expect((accountWallets as RepositoryError).message).toBe(WalletCurrency.Btc) }) @@ -78,7 +80,9 @@ describe("WalletsRepository", () => { await newWallet(WalletCurrency.Usd) const accountWallets = await wallets.findAccountWalletsByAccountId(accountId) - expect(accountWallets).toBeInstanceOf(MultipleWalletsFoundForAccountIdAndCurrency) + expect(accountWallets).toBeInstanceOf( + MultipleWalletsFoundForAccountIdAndCurrencyError, + ) expect((accountWallets as RepositoryError).message).toBe(WalletCurrency.Usd) }) })