@@ -28,7 +28,10 @@ import static bubble.server.BubbleServer.isRestoreMode; | |||
public class BubbleAuthFilter extends AuthFilter<Account> { | |||
public static final Set<String> SKIP_AUTH_PREFIXES = new HashSet<>(Arrays.asList( | |||
AUTH_ENDPOINT, ENTITY_CONFIGS_ENDPOINT, BUBBLE_MAGIC_ENDPOINT, MESSAGES_ENDPOINT, TIMEZONES_ENDPOINT, | |||
AUTH_ENDPOINT, ENTITY_CONFIGS_ENDPOINT, | |||
PLANS_ENDPOINT, PAYMENT_METHODS_ENDPOINT, | |||
BUBBLE_MAGIC_ENDPOINT, | |||
MESSAGES_ENDPOINT, TIMEZONES_ENDPOINT, | |||
NOTIFY_ENDPOINT, FILTER_HTTP_ENDPOINT, DETECT_ENDPOINT | |||
)); | |||
public static final Set<String> SKIP_AUTH_PATHS = new SingletonSet<>(AUTH_ENDPOINT); | |||
@@ -110,9 +110,18 @@ public class StripePaymentDriver extends PaymentDriverBase<StripePaymentDriverCo | |||
final AccountPolicy policy = policyDAO.findSingleByAccount(accountPaymentMethod.getAccount()); | |||
if (policy == null) return new PaymentValidationResult("err.paymentInfo.emailRequired"); | |||
final String email = policy.getFirstVerifiedEmail(); | |||
if (email == null && policy.getFirstEmail() != null) { | |||
return new PaymentValidationResult("err.paymentInfo.verifiedEmailRequired"); | |||
final String email; | |||
if (accountPaymentMethod.requireValidatedEmail()) { | |||
email = policy.getFirstVerifiedEmail(); | |||
if (email == null && policy.getFirstEmail() != null) { | |||
return new PaymentValidationResult("err.paymentInfo.verifiedEmailRequired"); | |||
} | |||
} else { | |||
email = policy.getFirstEmail(); | |||
if (email == null) { | |||
return new PaymentValidationResult("err.paymentInfo.verifiedEmailRequired"); | |||
} | |||
} | |||
final Map<String, Object> customerParams = new HashMap<>(); | |||
@@ -46,8 +46,6 @@ import static bubble.server.BubbleConfiguration.getDEFAULT_LOCALE; | |||
import static java.lang.Thread.currentThread; | |||
import static java.util.concurrent.TimeUnit.MINUTES; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.daemon; | |||
import static org.cobbzilla.util.json.JsonUtil.COMPACT_MAPPER; | |||
import static org.cobbzilla.util.json.JsonUtil.json; | |||
import static org.cobbzilla.wizard.model.IdentifiableBase.CTIME_ASC; | |||
import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; | |||
@@ -257,11 +255,9 @@ public class AccountDAO extends AbstractCRUDDAO<Account> implements SqlViewSearc | |||
return ((AppSiteDAO) dao).findByAccountAndAppAndId(accountUuid, apps.get(parentEntity.getApp()).getUuid(), parentEntity.getName()); | |||
} | |||
@Override public AppSite preCreate(AppSite parentEntity, AppSite accountEntity) { | |||
log.info("CopyTemplate.AppSite.preCreate: site="+json(accountEntity, COMPACT_MAPPER)); | |||
return accountEntity.setApp(apps.get(parentEntity.getApp()).getUuid()); | |||
} | |||
@Override public void postCreate(AppSite parentEntity, AppSite accountEntity) { | |||
log.info("CopyTemplate.AppSite.postCreate: site="+json(accountEntity, COMPACT_MAPPER)); | |||
sites.put(parentEntity.getUuid(), accountEntity); | |||
} | |||
}); | |||
@@ -27,6 +27,7 @@ public class AccountInitializer implements Runnable { | |||
public static final int MAX_ACCOUNT_INIT_RETRIES = 3; | |||
public static final long COPY_WAIT_TIME = SECONDS.toMillis(2); | |||
public static final long SEND_MESSAGE_WAIT_TIME = SECONDS.toMillis(1); | |||
private Account account; | |||
private AccountDAO accountDAO; | |||
@@ -36,6 +37,15 @@ public class AccountInitializer implements Runnable { | |||
private AtomicBoolean ready = new AtomicBoolean(false); | |||
public boolean ready() { return ready.get(); } | |||
private AtomicBoolean canSendAccountMessages = new AtomicBoolean(false); | |||
public void setCanSendAccountMessages() { canSendAccountMessages.set(true); } | |||
private AtomicBoolean abort = new AtomicBoolean(false); | |||
public void setAbort () { abort.set(true); } | |||
private AtomicBoolean completed = new AtomicBoolean(false); | |||
public boolean completed () { return completed.get(); } | |||
private AtomicReference<Exception> error = new AtomicReference<>(); | |||
public Exception getError() { return error.get(); } | |||
public boolean hasError () { return getError() != null; } | |||
@@ -56,6 +66,14 @@ public class AccountInitializer implements Runnable { | |||
sleep(COPY_WAIT_TIME, "waiting before copyTemplates"); | |||
accountDAO.copyTemplates(account, ready); | |||
while (!canSendAccountMessages.get() && !abort.get()) { | |||
sleep(SEND_MESSAGE_WAIT_TIME, "waiting before sending welcome message"); | |||
} | |||
if (abort.get()) { | |||
log.warn("aborting!"); | |||
return; | |||
} | |||
if (account.hasPolicy() && account.getPolicy().hasAccountContacts()) { | |||
messageDAO.sendVerifyRequest(account.getRemoteHost(), account, account.getPolicy().getAccountContacts()[0]); | |||
} | |||
@@ -82,6 +100,8 @@ public class AccountInitializer implements Runnable { | |||
error.set(e); | |||
// todo: send to errbit | |||
die("error: "+e, e); | |||
} finally { | |||
completed.set(true); | |||
} | |||
} | |||
} |
@@ -5,6 +5,7 @@ | |||
package bubble.dao.bill; | |||
import bubble.cloud.payment.PaymentServiceDriver; | |||
import bubble.dao.account.AccountDAO; | |||
import bubble.dao.account.AccountOwnedEntityDAO; | |||
import bubble.dao.cloud.BubbleNetworkDAO; | |||
import bubble.dao.cloud.CloudServiceDAO; | |||
@@ -39,6 +40,7 @@ public class AccountPlanDAO extends AccountOwnedEntityDAO<AccountPlan> { | |||
public static final long PURCHASE_DELAY = SECONDS.toMillis(3); | |||
@Autowired private BubblePlanDAO planDAO; | |||
@Autowired private AccountDAO accountDAO; | |||
@Autowired private BillDAO billDAO; | |||
@Autowired private CloudServiceDAO cloudDAO; | |||
@Autowired private BubbleNetworkDAO networkDAO; | |||
@@ -77,7 +79,7 @@ public class AccountPlanDAO extends AccountOwnedEntityDAO<AccountPlan> { | |||
} | |||
@Override public Object preCreate(AccountPlan accountPlan) { | |||
final ValidationResult errors = validateHostname(accountPlan); | |||
final ValidationResult errors = validateHostname(accountPlan, accountDAO, networkDAO); | |||
if (errors.isInvalid()) throw invalidEx(errors); | |||
if (configuration.paymentsEnabled()) { | |||
@@ -86,7 +86,8 @@ public class Account extends IdentifiableBaseParentEntity implements TokenPrinci | |||
public static final String[] UPDATE_FIELDS = {"url", "description", "autoUpdatePolicy"}; | |||
public static final String[] ADMIN_UPDATE_FIELDS = ArrayUtil.append(UPDATE_FIELDS, "suspended", "admin"); | |||
public static final String[] CREATE_FIELDS = ArrayUtil.append(ADMIN_UPDATE_FIELDS, "name", "referralCode", "termsAgreed"); | |||
public static final String[] CREATE_FIELDS = ArrayUtil.append(ADMIN_UPDATE_FIELDS, | |||
"name", "referralCode", "termsAgreed", "preferredPlan"); | |||
public static final String ROOT_USERNAME = "root"; | |||
public static final int NAME_MIN_LENGTH = 4; | |||
@@ -191,6 +192,10 @@ public class Account extends IdentifiableBaseParentEntity implements TokenPrinci | |||
return new ConstraintViolationBean("err.password.invalid", "Password must contain at least one letter, one number, and one special character"); | |||
} | |||
@Column(length=UUID_MAXLEN) | |||
@Getter @Setter private String preferredPlan; | |||
public boolean hasPreferredPlan () { return !empty(preferredPlan); } | |||
@Embedded @Getter @Setter private AutoUpdatePolicy autoUpdatePolicy; | |||
public boolean wantsNewStuff () { return autoUpdatePolicy != null && autoUpdatePolicy.newStuff(); } | |||
@@ -4,6 +4,7 @@ | |||
*/ | |||
package bubble.model.account; | |||
import bubble.model.bill.AccountPaymentMethod; | |||
import lombok.Getter; | |||
import lombok.Setter; | |||
@@ -22,4 +23,6 @@ public class AccountRegistration extends Account { | |||
@Getter @Setter private Boolean agreeToTerms = null; | |||
public boolean agreeToTerms () { return agreeToTerms != null && agreeToTerms; } | |||
@Getter @Setter private AccountPaymentMethod paymentMethodObject; | |||
public boolean hasPaymentMethod () { return paymentMethodObject != null; } | |||
} |
@@ -98,6 +98,9 @@ public class AccountPaymentMethod extends IdentifiableBase implements HasAccount | |||
@JsonProperty @Override public long getCtime () { return super.getCtime(); } | |||
@Transient @Getter @Setter private transient Boolean requireValidatedEmail = null; | |||
public boolean requireValidatedEmail() { return requireValidatedEmail == null || requireValidatedEmail; } | |||
public ValidationResult validate(ValidationResult result, BubbleConfiguration configuration) { | |||
if (!hasPaymentMethodType()) { | |||
@@ -139,6 +142,7 @@ public class AccountPaymentMethod extends IdentifiableBase implements HasAccount | |||
if (empty(getPaymentInfo())) { | |||
result.addViolation("err.paymentInfo.required"); | |||
} else { | |||
log.info("validate: starting validation of payment method with this.requireValidatedEmail="+requireValidatedEmail); | |||
final PaymentValidationResult validationResult = paymentDriver.validate(this); | |||
if (validationResult.hasErrors()) { | |||
result.addAll(validationResult.getViolations()); | |||
@@ -202,12 +202,15 @@ public class BubbleNetwork extends IdentifiableBase implements HasNetwork, HasBu | |||
errors.addViolation("err.name.length"); | |||
} else if (name.length() < NETWORK_NAME_MINLEN) { | |||
errors.addViolation("err.name.tooShort"); | |||
} else if (networkDAO.findByNameAndDomainUuid(name, request.getDomain()) != null) { | |||
errors.addViolation("err.name.alreadyInUse"); | |||
} else { | |||
final Account acct = accountDAO.findByName(name); | |||
if (acct != null && !acct.getUuid().equals(request.getAccount())) { | |||
errors.addViolation("err.name.reservedForAccount"); | |||
final BubbleNetwork network = networkDAO.findByNameAndDomainUuid(name, request.getDomain()); | |||
if (network != null && !network.getUuid().equals(request.getNetwork())) { | |||
errors.addViolation("err.name.alreadyInUse"); | |||
} else { | |||
final Account acct = accountDAO.findByName(name); | |||
if (acct != null && !acct.getUuid().equals(request.getAccount())) { | |||
errors.addViolation("err.name.reservedForAccount"); | |||
} | |||
} | |||
} | |||
} | |||
@@ -111,20 +111,25 @@ public class AccountOwnedResource<E extends HasAccount, DAO extends AccountOwned | |||
public Response view(@Context ContainerRequest ctx, | |||
@PathParam("id") String id) { | |||
final Account caller = userPrincipal(ctx); | |||
final String accountUuid = getAccountUuid(ctx); | |||
if (!caller.admin() && !caller.getUuid().equals(accountUuid)) return notFound(); | |||
final Account caller = getAccountForViewById(ctx); | |||
E found = find(ctx, id); | |||
if (found == null) { | |||
found = findAlternate(ctx, id); | |||
if (found == null) return notFound(id); | |||
} | |||
if (!found.getAccount().equals(caller.getUuid()) && !caller.admin()) return notFound(id); | |||
if (caller != null && !found.getAccount().equals(caller.getUuid()) && !caller.admin()) return notFound(id); | |||
return ok(populate(ctx, found)); | |||
} | |||
public Account getAccountForViewById(ContainerRequest ctx) { | |||
final Account caller = userPrincipal(ctx); | |||
final String accountUuid = getAccountUuid(ctx); | |||
if (!caller.admin() && !caller.getUuid().equals(accountUuid)) throw notFoundEx(); | |||
return caller; | |||
} | |||
@PUT | |||
public Response create(@Context Request req, | |||
@Context ContainerRequest ctx, | |||
@@ -8,12 +8,15 @@ import bubble.dao.SessionDAO; | |||
import bubble.dao.account.AccountDAO; | |||
import bubble.dao.account.AccountPolicyDAO; | |||
import bubble.dao.account.message.AccountMessageDAO; | |||
import bubble.dao.bill.AccountPaymentMethodDAO; | |||
import bubble.dao.bill.BubblePlanDAO; | |||
import bubble.dao.cloud.BubbleNodeDAO; | |||
import bubble.dao.cloud.BubbleNodeKeyDAO; | |||
import bubble.model.CertType; | |||
import bubble.model.account.*; | |||
import bubble.model.account.message.*; | |||
import bubble.model.bill.AccountPaymentMethod; | |||
import bubble.model.bill.BubblePlan; | |||
import bubble.model.boot.ActivationRequest; | |||
import bubble.model.cloud.BubbleNetwork; | |||
import bubble.model.cloud.BubbleNode; | |||
@@ -23,8 +26,8 @@ import bubble.model.cloud.notify.NotificationReceipt; | |||
import bubble.model.device.BubbleDeviceType; | |||
import bubble.model.device.Device; | |||
import bubble.server.BubbleConfiguration; | |||
import bubble.service.account.StandardAuthenticatorService; | |||
import bubble.service.account.StandardAccountMessageService; | |||
import bubble.service.account.StandardAuthenticatorService; | |||
import bubble.service.backup.RestoreService; | |||
import bubble.service.bill.PromotionService; | |||
import bubble.service.boot.ActivationService; | |||
@@ -63,6 +66,8 @@ import static java.util.concurrent.TimeUnit.SECONDS; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.*; | |||
import static org.cobbzilla.util.http.HttpContentTypes.APPLICATION_JSON; | |||
import static org.cobbzilla.util.http.HttpContentTypes.CONTENT_TYPE_ANY; | |||
import static org.cobbzilla.util.json.JsonUtil.COMPACT_MAPPER; | |||
import static org.cobbzilla.util.json.JsonUtil.json; | |||
import static org.cobbzilla.util.string.LocaleUtil.currencyForLocale; | |||
import static org.cobbzilla.util.system.Sleep.sleep; | |||
import static org.cobbzilla.wizard.resources.ResourceUtil.*; | |||
@@ -81,6 +86,7 @@ public class AuthResource { | |||
@Autowired private SessionDAO sessionDAO; | |||
@Autowired private ActivationService activationService; | |||
@Autowired private AccountMessageDAO accountMessageDAO; | |||
@Autowired private AccountPaymentMethodDAO accountPaymentMethodDAO; | |||
@Autowired private StandardAccountMessageService messageService; | |||
@Autowired private BubblePlanDAO planDAO; | |||
@Autowired private BubbleNodeDAO nodeDAO; | |||
@@ -240,6 +246,11 @@ public class AuthResource { | |||
} | |||
} | |||
if (request.hasPreferredPlan()) { | |||
final BubblePlan plan = planDAO.findById(request.getPreferredPlan()); | |||
if (plan == null) errors.addViolation("err.plan.notFound"); | |||
} | |||
if (errors.isInvalid()) return invalid(errors); | |||
final String parentUuid = thisNetwork.getTag(TAG_PARENT_ACCOUNT, thisNetwork.getAccount()); | |||
@@ -249,12 +260,37 @@ public class AuthResource { | |||
final Account account = accountDAO.newAccount(req, null, request, parent); | |||
SimpleViolationException promoEx = null; | |||
if (configuration.paymentsEnabled()) { | |||
if (request.hasPaymentMethod()) { | |||
final AccountPaymentMethod paymentMethodObject = request.getPaymentMethodObject(); | |||
log.info("register: found AccountPaymentMethod at registration-time: " + json(paymentMethodObject, COMPACT_MAPPER)); | |||
paymentMethodObject.setUuid(null); | |||
paymentMethodObject.setAccount(account.getUuid()); | |||
paymentMethodObject.setRequireValidatedEmail(false); | |||
account.waitForAccountInit(); // payment clouds for user must exist before we can create the APM | |||
final ValidationResult result = new ValidationResult(); | |||
log.info("register: starting validation of payment method with requireValidatedEmail="+paymentMethodObject.requireValidatedEmail()); | |||
paymentMethodObject.validate(result, configuration); | |||
if (result.isInvalid()) { | |||
account.getAccountInitializer().setAbort(); | |||
final AccountPolicy policy = policyDAO.findSingleByAccount(account.getUuid()); | |||
policyDAO.update(policy.setDeletionPolicy(AccountDeletionPolicy.full_delete)); | |||
while (!account.getAccountInitializer().completed()) { | |||
sleep(SECONDS.toMillis(1), "waiting for account initialization to complete before deleting"); | |||
} | |||
accountDAO.delete(account.getUuid()); | |||
throw invalidEx(result); | |||
} | |||
log.info("register: creating AccountPaymentMethod upon registration: " + json(paymentMethodObject, COMPACT_MAPPER)); | |||
final AccountPaymentMethod apm = accountPaymentMethodDAO.create(paymentMethodObject); | |||
log.info("register: created AccountPaymentMethod upon registration: " + apm.getUuid()); | |||
} | |||
try { | |||
promoService.applyPromotions(account, request.getPromoCode(), currency); | |||
} catch (SimpleViolationException e) { | |||
promoEx = e; | |||
} | |||
} | |||
account.getAccountInitializer().setCanSendAccountMessages(); | |||
return ok(account | |||
.waitForAccountInit() | |||
.setPromoError(promoEx == null ? null : promoEx.getMessageTemplate()) | |||
@@ -5,6 +5,7 @@ | |||
package bubble.resources.bill; | |||
import bubble.cloud.CloudServiceType; | |||
import bubble.dao.account.AccountDAO; | |||
import bubble.dao.cloud.CloudServiceDAO; | |||
import bubble.model.account.Account; | |||
import bubble.model.bill.PaymentMethodType; | |||
@@ -33,13 +34,16 @@ import static org.cobbzilla.wizard.resources.ResourceUtil.*; | |||
public class AllPaymentMethodsResource { | |||
@Autowired private CloudServiceDAO cloudDAO; | |||
@Autowired private AccountDAO accountDAO; | |||
@Autowired private BubbleConfiguration configuration; | |||
@GET | |||
public Response listPaymentMethods(@Context ContainerRequest ctx, | |||
@QueryParam("type") PaymentMethodType type) { | |||
final Account account = userPrincipal(ctx); | |||
final List<CloudService> allPaymentServices = cloudDAO.findByAccountAndType(account.getUuid(), CloudServiceType.payment); | |||
final Account account = optionalUserPrincipal(ctx); | |||
final List<CloudService> allPaymentServices = account != null | |||
? cloudDAO.findByAccountAndType(account.getUuid(), CloudServiceType.payment) | |||
: cloudDAO.findPublicTemplatesByType(accountDAO.getFirstAdmin().getUuid(), CloudServiceType.payment); | |||
final Set<PaymentMethodType> typesFound = new HashSet<>(); | |||
final List<CloudService> paymentServices = new ArrayList<>(); | |||
for (CloudService cloud : allPaymentServices) { | |||
@@ -41,6 +41,11 @@ public class BubblePlansResource extends AccountOwnedResource<BubblePlan, Bubble | |||
@Autowired private BubblePlanAppDAO planAppDAO; | |||
@Autowired private BubbleAppDAO appDAO; | |||
// allow unauthenticated users to read plans | |||
@Override public Account getAccountForViewById(ContainerRequest ctx) { | |||
return optionalUserPrincipal(ctx); | |||
} | |||
@Override protected BubblePlan setReferences(ContainerRequest ctx, Account caller, BubblePlan bubblePlan) { | |||
if (empty(bubblePlan.getChargeName())) throw invalidEx("err.chargeName.required"); | |||
if (bubblePlan.getChargeName().length() > MAX_CHARGENAME_LEN) throw invalidEx("err.chargeName.length"); | |||
@@ -67,8 +72,12 @@ public class BubblePlansResource extends AccountOwnedResource<BubblePlan, Bubble | |||
} | |||
@Override protected BubblePlan populate(ContainerRequest ctx, BubblePlan plan) { | |||
final Account account = optionalUserPrincipal(ctx); | |||
final List<BubbleApp> apps = getAppsForPlan(plan); | |||
plan.setApps(apps); | |||
if (account == null) { | |||
plan.getApps().forEach(app -> app.setDataConfig(null)); | |||
} | |||
return super.populate(ctx, plan); | |||
} | |||
@@ -310,26 +310,6 @@ footprint_description_EU=Your Bubble will only run within European Union countri | |||
footprint_name_Worldwide=World-wide | |||
footprint_description_Worldwide=Your Bubble can run anywhere in the world | |||
# Payment methods | |||
payment_description_credit=Credit or Debit Card | |||
payment_description_code=Invitation Code | |||
payment_description_free=FREE! | |||
message_payment_not_supported=This system is not configured to handle payments of this type | |||
# Invite code payment fields | |||
field_payment_invite_code=Invitation Code | |||
button_label_submit_invite_code=Use Invite Code | |||
message_verified_invite_code=Invite Code Successfully Verified | |||
# Free payment fields | |||
button_label_submit_free_pay=Click here to use the FREE payment method | |||
message_verified_free_pay=Free Payment Successfully Applied | |||
# Credit payment fields | |||
field_payment_card_number=Credit or Debit Card | |||
button_label_submit_card=Verify Card | |||
message_verified_card=Card Successfully Verified | |||
# Launch progress meter: pre-launch (standard) ticks | |||
meter_tick_confirming_network_lock=Confirming network lock | |||
meter_tick_validating_node_network_and_plan=Verifying settings for Bubble | |||
@@ -268,6 +268,8 @@ form_title_register=Register | |||
field_label_contactType=Contact Type | |||
field_label_email=Email | |||
field_label_promoCode=Beta Invite Code | |||
message_request_promoCode=Don't have an invite code? Request one here. | |||
message_request_promoCode_link=https://bubblev.com/beta | |||
field_label_sms=SMS Phone | |||
field_label_agreeToTerms=By checking this box, you agree to our <a target="_blank" rel="noopener" href="https://bubblev.com/pages/privacy">Privacy Policy</a> and <a target="_blank" rel="noopener" href="https://bubblev.com/pages/terms">Terms of Service</a>. | |||
message_login_agreeToTerms=By logging in, you agree to the <a target="_blank" rel="noopener" href="https://bubblev.com/pages/privacy">Privacy Policy</a> and <a target="_blank" rel="noopener" href="https://bubblev.com/pages/terms">Terms of Service</a>. | |||
@@ -278,6 +280,8 @@ field_label_paymentMethod=Payment Method | |||
field_label_newPaymentMethod=Add a Payment Method | |||
field_label_existingPaymentMethod=Payment Method | |||
err_noPaymentMethods=No payment methods are configured. Contact the administrator of this system. | |||
err.paymentMethod.required=Payment information is required | |||
err.plan.notFound=Plan not found | |||
button_label_login=Login | |||
button_label_register=Register | |||
button_label_forgotPassword=Forgot Password | |||
@@ -287,6 +291,26 @@ form_title_forgotPassword=Forgot Password | |||
button_label_resetPassword=Request Password Reset | |||
message_resetPassword_sent=Password Reset Message Successfully Sent | |||
# Payment methods | |||
payment_description_credit=Credit or Debit Card | |||
payment_description_code=Invitation Code | |||
payment_description_free=FREE! | |||
message_payment_not_supported=This system is not configured to handle payments of this type | |||
# Invite code payment fields | |||
field_payment_invite_code=Invitation Code | |||
button_label_submit_invite_code=Use Invite Code | |||
message_verified_invite_code=Invite Code Successfully Verified | |||
# Free payment fields | |||
button_label_submit_free_pay=Click here to use the FREE payment method | |||
message_verified_free_pay=Free Payment Successfully Applied | |||
# Credit payment fields | |||
field_payment_card_number=Credit or Debit Card | |||
button_label_submit_card=Verify Card | |||
message_verified_card=Card Successfully Verified | |||
# Change Password / Set Password pages | |||
form_title_change_password=Change Password | |||
form_title_set_password=Set Password | |||
@@ -1 +1 @@ | |||
Subproject commit 2da83c231b0a07b723ba60e33e2e1a540311cceb | |||
Subproject commit 372ad8a74fddeab1cb174685bd7f2cde919ac48e |