def manual_upgrade(way: str, ids: [int], is_giveaway: bool): for user in User.filter(User.id.in_(ids)).all(): if user.lifetime: flash(f"user {user} already has a lifetime license", "warning") continue sub: Subscription = user.get_subscription() if sub and not sub.cancelled: flash( f"user {user} already has a Paddle license, they have to cancel it first", "warning", ) continue apple_sub: AppleSubscription = AppleSubscription.get_by( user_id=user.id) if apple_sub and apple_sub.is_valid(): flash( f"user {user} already has a Apple subscription, they have to cancel it first", "warning", ) continue manual_sub: ManualSubscription = ManualSubscription.get_by( user_id=user.id) if manual_sub: # renew existing subscription if manual_sub.end_at > arrow.now(): manual_sub.end_at = manual_sub.end_at.shift(years=1) else: manual_sub.end_at = arrow.now().shift(years=1, days=1) Session.commit() flash(f"Subscription extended to {manual_sub.end_at.humanize()}", "success") continue ManualSubscription.create( user_id=user.id, end_at=arrow.now().shift(years=1, days=1), comment=way, is_giveaway=is_giveaway, commit=True, ) flash(f"New {way} manual subscription for {user} is created", "success")
def pricing(): if current_user.lifetime: flash("You already have a lifetime subscription", "error") return redirect(url_for("dashboard.index")) sub: Subscription = current_user.get_subscription() # user who has canceled can re-subscribe if sub and not sub.cancelled: flash("You already have an active subscription", "error") return redirect(url_for("dashboard.index")) now = arrow.now() manual_sub: ManualSubscription = ManualSubscription.filter( ManualSubscription.user_id == current_user.id, ManualSubscription.end_at > now).first() coinbase_sub = CoinbaseSubscription.filter( CoinbaseSubscription.user_id == current_user.id, CoinbaseSubscription.end_at > now, ).first() apple_sub: AppleSubscription = AppleSubscription.get_by( user_id=current_user.id) if apple_sub and apple_sub.is_valid(): flash("Please make sure to cancel your subscription on Apple first", "warning") return render_template( "dashboard/pricing.html", PADDLE_VENDOR_ID=PADDLE_VENDOR_ID, PADDLE_MONTHLY_PRODUCT_ID=PADDLE_MONTHLY_PRODUCT_ID, PADDLE_YEARLY_PRODUCT_ID=PADDLE_YEARLY_PRODUCT_ID, success_url=URL + "/dashboard/subscription_success", manual_sub=manual_sub, coinbase_sub=coinbase_sub, now=now, )
def apple_update_notification(): """ The "Subscription Status URL" to receive update notifications from Apple """ # request.json looks like this # will use unified_receipt.latest_receipt_info and NOT latest_expired_receipt_info # more info on https://developer.apple.com/documentation/appstoreservernotifications/responsebody # { # "unified_receipt": { # "latest_receipt": "long string", # "pending_renewal_info": [ # { # "is_in_billing_retry_period": "0", # "auto_renew_status": "0", # "original_transaction_id": "1000000654277043", # "product_id": "io.simplelogin.ios_app.subscription.premium.yearly", # "expiration_intent": "1", # "auto_renew_product_id": "io.simplelogin.ios_app.subscription.premium.yearly", # } # ], # "environment": "Sandbox", # "status": 0, # "latest_receipt_info": [ # { # "expires_date_pst": "2020-04-20 21:11:57 America/Los_Angeles", # "purchase_date": "2020-04-21 03:11:57 Etc/GMT", # "purchase_date_ms": "1587438717000", # "original_purchase_date_ms": "1587420715000", # "transaction_id": "1000000654329911", # "original_transaction_id": "1000000654277043", # "quantity": "1", # "expires_date_ms": "1587442317000", # "original_purchase_date_pst": "2020-04-20 15:11:55 America/Los_Angeles", # "product_id": "io.simplelogin.ios_app.subscription.premium.yearly", # "subscription_group_identifier": "20624274", # "web_order_line_item_id": "1000000051891577", # "expires_date": "2020-04-21 04:11:57 Etc/GMT", # "is_in_intro_offer_period": "false", # "original_purchase_date": "2020-04-20 22:11:55 Etc/GMT", # "purchase_date_pst": "2020-04-20 20:11:57 America/Los_Angeles", # "is_trial_period": "false", # }, # { # "expires_date_pst": "2020-04-20 20:11:57 America/Los_Angeles", # "purchase_date": "2020-04-21 02:11:57 Etc/GMT", # "purchase_date_ms": "1587435117000", # "original_purchase_date_ms": "1587420715000", # "transaction_id": "1000000654313889", # "original_transaction_id": "1000000654277043", # "quantity": "1", # "expires_date_ms": "1587438717000", # "original_purchase_date_pst": "2020-04-20 15:11:55 America/Los_Angeles", # "product_id": "io.simplelogin.ios_app.subscription.premium.yearly", # "subscription_group_identifier": "20624274", # "web_order_line_item_id": "1000000051890729", # "expires_date": "2020-04-21 03:11:57 Etc/GMT", # "is_in_intro_offer_period": "false", # "original_purchase_date": "2020-04-20 22:11:55 Etc/GMT", # "purchase_date_pst": "2020-04-20 19:11:57 America/Los_Angeles", # "is_trial_period": "false", # }, # { # "expires_date_pst": "2020-04-20 19:11:54 America/Los_Angeles", # "purchase_date": "2020-04-21 01:11:54 Etc/GMT", # "purchase_date_ms": "1587431514000", # "original_purchase_date_ms": "1587420715000", # "transaction_id": "1000000654300800", # "original_transaction_id": "1000000654277043", # "quantity": "1", # "expires_date_ms": "1587435114000", # "original_purchase_date_pst": "2020-04-20 15:11:55 America/Los_Angeles", # "product_id": "io.simplelogin.ios_app.subscription.premium.yearly", # "subscription_group_identifier": "20624274", # "web_order_line_item_id": "1000000051890161", # "expires_date": "2020-04-21 02:11:54 Etc/GMT", # "is_in_intro_offer_period": "false", # "original_purchase_date": "2020-04-20 22:11:55 Etc/GMT", # "purchase_date_pst": "2020-04-20 18:11:54 America/Los_Angeles", # "is_trial_period": "false", # }, # { # "expires_date_pst": "2020-04-20 18:11:54 America/Los_Angeles", # "purchase_date": "2020-04-21 00:11:54 Etc/GMT", # "purchase_date_ms": "1587427914000", # "original_purchase_date_ms": "1587420715000", # "transaction_id": "1000000654293615", # "original_transaction_id": "1000000654277043", # "quantity": "1", # "expires_date_ms": "1587431514000", # "original_purchase_date_pst": "2020-04-20 15:11:55 America/Los_Angeles", # "product_id": "io.simplelogin.ios_app.subscription.premium.yearly", # "subscription_group_identifier": "20624274", # "web_order_line_item_id": "1000000051889539", # "expires_date": "2020-04-21 01:11:54 Etc/GMT", # "is_in_intro_offer_period": "false", # "original_purchase_date": "2020-04-20 22:11:55 Etc/GMT", # "purchase_date_pst": "2020-04-20 17:11:54 America/Los_Angeles", # "is_trial_period": "false", # }, # { # "expires_date_pst": "2020-04-20 17:11:54 America/Los_Angeles", # "purchase_date": "2020-04-20 23:11:54 Etc/GMT", # "purchase_date_ms": "1587424314000", # "original_purchase_date_ms": "1587420715000", # "transaction_id": "1000000654285464", # "original_transaction_id": "1000000654277043", # "quantity": "1", # "expires_date_ms": "1587427914000", # "original_purchase_date_pst": "2020-04-20 15:11:55 America/Los_Angeles", # "product_id": "io.simplelogin.ios_app.subscription.premium.yearly", # "subscription_group_identifier": "20624274", # "web_order_line_item_id": "1000000051888827", # "expires_date": "2020-04-21 00:11:54 Etc/GMT", # "is_in_intro_offer_period": "false", # "original_purchase_date": "2020-04-20 22:11:55 Etc/GMT", # "purchase_date_pst": "2020-04-20 16:11:54 America/Los_Angeles", # "is_trial_period": "false", # }, # { # "expires_date_pst": "2020-04-20 16:11:54 America/Los_Angeles", # "purchase_date": "2020-04-20 22:11:54 Etc/GMT", # "purchase_date_ms": "1587420714000", # "original_purchase_date_ms": "1587420715000", # "transaction_id": "1000000654277043", # "original_transaction_id": "1000000654277043", # "quantity": "1", # "expires_date_ms": "1587424314000", # "original_purchase_date_pst": "2020-04-20 15:11:55 America/Los_Angeles", # "product_id": "io.simplelogin.ios_app.subscription.premium.yearly", # "subscription_group_identifier": "20624274", # "web_order_line_item_id": "1000000051888825", # "expires_date": "2020-04-20 23:11:54 Etc/GMT", # "is_in_intro_offer_period": "false", # "original_purchase_date": "2020-04-20 22:11:55 Etc/GMT", # "purchase_date_pst": "2020-04-20 15:11:54 America/Los_Angeles", # "is_trial_period": "false", # }, # ], # }, # "auto_renew_status_change_date": "2020-04-21 04:11:33 Etc/GMT", # "environment": "Sandbox", # "auto_renew_status": "false", # "auto_renew_status_change_date_pst": "2020-04-20 21:11:33 America/Los_Angeles", # "latest_expired_receipt": "long string", # "latest_expired_receipt_info": { # "original_purchase_date_pst": "2020-04-20 15:11:55 America/Los_Angeles", # "quantity": "1", # "subscription_group_identifier": "20624274", # "unique_vendor_identifier": "4C4DF6BA-DE2A-4737-9A68-5992338886DC", # "original_purchase_date_ms": "1587420715000", # "expires_date_formatted": "2020-04-21 04:11:57 Etc/GMT", # "is_in_intro_offer_period": "false", # "purchase_date_ms": "1587438717000", # "expires_date_formatted_pst": "2020-04-20 21:11:57 America/Los_Angeles", # "is_trial_period": "false", # "item_id": "1508744966", # "unique_identifier": "b55fc3dcc688e979115af0697a0195be78be7cbd", # "original_transaction_id": "1000000654277043", # "expires_date": "1587442317000", # "transaction_id": "1000000654329911", # "bvrs": "3", # "web_order_line_item_id": "1000000051891577", # "version_external_identifier": "834289833", # "bid": "io.simplelogin.ios-app", # "product_id": "io.simplelogin.ios_app.subscription.premium.yearly", # "purchase_date": "2020-04-21 03:11:57 Etc/GMT", # "purchase_date_pst": "2020-04-20 20:11:57 America/Los_Angeles", # "original_purchase_date": "2020-04-20 22:11:55 Etc/GMT", # }, # "password": "******", # "auto_renew_status_change_date_ms": "1587442293000", # "auto_renew_product_id": "io.simplelogin.ios_app.subscription.premium.yearly", # "notification_type": "DID_CHANGE_RENEWAL_STATUS", # } LOG.debug("request for /api/apple/update_notification") data = request.get_json() if not ( data and data.get("unified_receipt") and data["unified_receipt"].get("latest_receipt_info") ): LOG.d("Invalid data %s", data) return jsonify(error="Empty Response"), 400 transactions = data["unified_receipt"]["latest_receipt_info"] # dict of original_transaction_id and transaction latest_transactions = {} for transaction in transactions: original_transaction_id = transaction["original_transaction_id"] if not latest_transactions.get(original_transaction_id): latest_transactions[original_transaction_id] = transaction if ( transaction["expires_date_ms"] > latest_transactions[original_transaction_id]["expires_date_ms"] ): latest_transactions[original_transaction_id] = transaction for original_transaction_id, transaction in latest_transactions.items(): expires_date = arrow.get(int(transaction["expires_date_ms"]) / 1000) plan = ( PlanEnum.monthly if transaction["product_id"] in (_MONTHLY_PRODUCT_ID, _MACAPP_MONTHLY_PRODUCT_ID) else PlanEnum.yearly ) apple_sub: AppleSubscription = AppleSubscription.get_by( original_transaction_id=original_transaction_id ) if apple_sub: user = apple_sub.user LOG.d( "Update AppleSubscription for user %s, expired at %s, plan %s", user, expires_date, plan, ) apple_sub.receipt_data = data["unified_receipt"]["latest_receipt"] apple_sub.expires_date = expires_date apple_sub.plan = plan db.session.commit() return jsonify(ok=True), 200 else: LOG.warning( "No existing AppleSub for original_transaction_id %s", original_transaction_id, ) LOG.d("request data %s", data) return jsonify(ok=False), 400
def verify_receipt(receipt_data, user, password) -> Optional[AppleSubscription]: """Call verifyReceipt endpoint and create/update AppleSubscription table Call the production URL for verifyReceipt first, and proceed to verify with the sandbox URL if receive a 21007 status code. Return AppleSubscription object if success https://developer.apple.com/documentation/appstorereceipts/verifyreceipt """ LOG.d("start verify_receipt") try: r = requests.post( _PROD_URL, json={"receipt-data": receipt_data, "password": password} ) except ConnectionError: LOG.warning("cannot call Apple server %s", _PROD_URL) return None if r.status_code >= 500: LOG.warning("Apple server error, response:%s %s", r, r.content) return None if r.json() == {"status": 21007}: # try sandbox_url LOG.warning("Use the sandbox url instead") r = requests.post( _SANDBOX_URL, json={"receipt-data": receipt_data, "password": password}, ) data = r.json() # data has the following format # { # "status": 0, # "environment": "Sandbox", # "receipt": { # "receipt_type": "ProductionSandbox", # "adam_id": 0, # "app_item_id": 0, # "bundle_id": "io.simplelogin.ios-app", # "application_version": "2", # "download_id": 0, # "version_external_identifier": 0, # "receipt_creation_date": "2020-04-18 16:36:34 Etc/GMT", # "receipt_creation_date_ms": "1587227794000", # "receipt_creation_date_pst": "2020-04-18 09:36:34 America/Los_Angeles", # "request_date": "2020-04-18 16:46:36 Etc/GMT", # "request_date_ms": "1587228396496", # "request_date_pst": "2020-04-18 09:46:36 America/Los_Angeles", # "original_purchase_date": "2013-08-01 07:00:00 Etc/GMT", # "original_purchase_date_ms": "1375340400000", # "original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles", # "original_application_version": "1.0", # "in_app": [ # { # "quantity": "1", # "product_id": "io.simplelogin.ios_app.subscription.premium.monthly", # "transaction_id": "1000000653584474", # "original_transaction_id": "1000000653584474", # "purchase_date": "2020-04-18 16:27:42 Etc/GMT", # "purchase_date_ms": "1587227262000", # "purchase_date_pst": "2020-04-18 09:27:42 America/Los_Angeles", # "original_purchase_date": "2020-04-18 16:27:44 Etc/GMT", # "original_purchase_date_ms": "1587227264000", # "original_purchase_date_pst": "2020-04-18 09:27:44 America/Los_Angeles", # "expires_date": "2020-04-18 16:32:42 Etc/GMT", # "expires_date_ms": "1587227562000", # "expires_date_pst": "2020-04-18 09:32:42 America/Los_Angeles", # "web_order_line_item_id": "1000000051847459", # "is_trial_period": "false", # "is_in_intro_offer_period": "false", # }, # { # "quantity": "1", # "product_id": "io.simplelogin.ios_app.subscription.premium.monthly", # "transaction_id": "1000000653584861", # "original_transaction_id": "1000000653584474", # "purchase_date": "2020-04-18 16:32:42 Etc/GMT", # "purchase_date_ms": "1587227562000", # "purchase_date_pst": "2020-04-18 09:32:42 America/Los_Angeles", # "original_purchase_date": "2020-04-18 16:27:44 Etc/GMT", # "original_purchase_date_ms": "1587227264000", # "original_purchase_date_pst": "2020-04-18 09:27:44 America/Los_Angeles", # "expires_date": "2020-04-18 16:37:42 Etc/GMT", # "expires_date_ms": "1587227862000", # "expires_date_pst": "2020-04-18 09:37:42 America/Los_Angeles", # "web_order_line_item_id": "1000000051847461", # "is_trial_period": "false", # "is_in_intro_offer_period": "false", # }, # ], # }, # "latest_receipt_info": [ # { # "quantity": "1", # "product_id": "io.simplelogin.ios_app.subscription.premium.monthly", # "transaction_id": "1000000653584474", # "original_transaction_id": "1000000653584474", # "purchase_date": "2020-04-18 16:27:42 Etc/GMT", # "purchase_date_ms": "1587227262000", # "purchase_date_pst": "2020-04-18 09:27:42 America/Los_Angeles", # "original_purchase_date": "2020-04-18 16:27:44 Etc/GMT", # "original_purchase_date_ms": "1587227264000", # "original_purchase_date_pst": "2020-04-18 09:27:44 America/Los_Angeles", # "expires_date": "2020-04-18 16:32:42 Etc/GMT", # "expires_date_ms": "1587227562000", # "expires_date_pst": "2020-04-18 09:32:42 America/Los_Angeles", # "web_order_line_item_id": "1000000051847459", # "is_trial_period": "false", # "is_in_intro_offer_period": "false", # "subscription_group_identifier": "20624274", # }, # { # "quantity": "1", # "product_id": "io.simplelogin.ios_app.subscription.premium.monthly", # "transaction_id": "1000000653584861", # "original_transaction_id": "1000000653584474", # "purchase_date": "2020-04-18 16:32:42 Etc/GMT", # "purchase_date_ms": "1587227562000", # "purchase_date_pst": "2020-04-18 09:32:42 America/Los_Angeles", # "original_purchase_date": "2020-04-18 16:27:44 Etc/GMT", # "original_purchase_date_ms": "1587227264000", # "original_purchase_date_pst": "2020-04-18 09:27:44 America/Los_Angeles", # "expires_date": "2020-04-18 16:37:42 Etc/GMT", # "expires_date_ms": "1587227862000", # "expires_date_pst": "2020-04-18 09:37:42 America/Los_Angeles", # "web_order_line_item_id": "1000000051847461", # "is_trial_period": "false", # "is_in_intro_offer_period": "false", # "subscription_group_identifier": "20624274", # }, # { # "quantity": "1", # "product_id": "io.simplelogin.ios_app.subscription.premium.monthly", # "transaction_id": "1000000653585235", # "original_transaction_id": "1000000653584474", # "purchase_date": "2020-04-18 16:38:16 Etc/GMT", # "purchase_date_ms": "1587227896000", # "purchase_date_pst": "2020-04-18 09:38:16 America/Los_Angeles", # "original_purchase_date": "2020-04-18 16:27:44 Etc/GMT", # "original_purchase_date_ms": "1587227264000", # "original_purchase_date_pst": "2020-04-18 09:27:44 America/Los_Angeles", # "expires_date": "2020-04-18 16:43:16 Etc/GMT", # "expires_date_ms": "1587228196000", # "expires_date_pst": "2020-04-18 09:43:16 America/Los_Angeles", # "web_order_line_item_id": "1000000051847500", # "is_trial_period": "false", # "is_in_intro_offer_period": "false", # "subscription_group_identifier": "20624274", # }, # { # "quantity": "1", # "product_id": "io.simplelogin.ios_app.subscription.premium.monthly", # "transaction_id": "1000000653585760", # "original_transaction_id": "1000000653584474", # "purchase_date": "2020-04-18 16:44:25 Etc/GMT", # "purchase_date_ms": "1587228265000", # "purchase_date_pst": "2020-04-18 09:44:25 America/Los_Angeles", # "original_purchase_date": "2020-04-18 16:27:44 Etc/GMT", # "original_purchase_date_ms": "1587227264000", # "original_purchase_date_pst": "2020-04-18 09:27:44 America/Los_Angeles", # "expires_date": "2020-04-18 16:49:25 Etc/GMT", # "expires_date_ms": "1587228565000", # "expires_date_pst": "2020-04-18 09:49:25 America/Los_Angeles", # "web_order_line_item_id": "1000000051847566", # "is_trial_period": "false", # "is_in_intro_offer_period": "false", # "subscription_group_identifier": "20624274", # }, # ], # "latest_receipt": "very long string", # "pending_renewal_info": [ # { # "auto_renew_product_id": "io.simplelogin.ios_app.subscription.premium.monthly", # "original_transaction_id": "1000000653584474", # "product_id": "io.simplelogin.ios_app.subscription.premium.monthly", # "auto_renew_status": "1", # } # ], # } if data["status"] != 0: LOG.warning( "verifyReceipt status !=0, probably invalid receipt. User %s", user, ) return None # each item in data["receipt"]["in_app"] has the following format # { # "quantity": "1", # "product_id": "io.simplelogin.ios_app.subscription.premium.monthly", # "transaction_id": "1000000653584474", # "original_transaction_id": "1000000653584474", # "purchase_date": "2020-04-18 16:27:42 Etc/GMT", # "purchase_date_ms": "1587227262000", # "purchase_date_pst": "2020-04-18 09:27:42 America/Los_Angeles", # "original_purchase_date": "2020-04-18 16:27:44 Etc/GMT", # "original_purchase_date_ms": "1587227264000", # "original_purchase_date_pst": "2020-04-18 09:27:44 America/Los_Angeles", # "expires_date": "2020-04-18 16:32:42 Etc/GMT", # "expires_date_ms": "1587227562000", # "expires_date_pst": "2020-04-18 09:32:42 America/Los_Angeles", # "web_order_line_item_id": "1000000051847459", # "is_trial_period": "false", # "is_in_intro_offer_period": "false", # } transactions = data["receipt"]["in_app"] if not transactions: LOG.warning("Empty transactions in data %s", data) return None latest_transaction = max(transactions, key=lambda t: int(t["expires_date_ms"])) original_transaction_id = latest_transaction["original_transaction_id"] expires_date = arrow.get(int(latest_transaction["expires_date_ms"]) / 1000) plan = ( PlanEnum.monthly if latest_transaction["product_id"] == _MONTHLY_PRODUCT_ID else PlanEnum.yearly ) apple_sub: AppleSubscription = AppleSubscription.get_by(user_id=user.id) if apple_sub: LOG.d( "Update AppleSubscription for user %s, expired at %s, plan %s", user, expires_date, plan, ) apple_sub.receipt_data = receipt_data apple_sub.expires_date = expires_date apple_sub.original_transaction_id = original_transaction_id apple_sub.plan = plan else: # the same original_transaction_id has been used on another account if AppleSubscription.get_by(original_transaction_id=original_transaction_id): LOG.exception("Same Apple Sub has been used before, current user %s", user) return None LOG.d( "Create new AppleSubscription for user %s, expired at %s, plan %s", user, expires_date, plan, ) apple_sub = AppleSubscription.create( user_id=user.id, receipt_data=receipt_data, expires_date=expires_date, original_transaction_id=original_transaction_id, plan=plan, ) db.session.commit() return apple_sub
def setting(): form = SettingForm() promo_form = PromoCodeForm() change_email_form = ChangeEmailForm() email_change = EmailChange.get_by(user_id=current_user.id) if email_change: pending_email = email_change.new_email else: pending_email = None if request.method == "POST": if request.form.get("form-name") == "update-email": if change_email_form.validate(): # whether user can proceed with the email update new_email_valid = True if (sanitize_email(change_email_form.email.data) != current_user.email and not pending_email): new_email = sanitize_email(change_email_form.email.data) # check if this email is not already used if personal_email_already_used(new_email) or Alias.get_by( email=new_email): flash(f"Email {new_email} already used", "error") new_email_valid = False elif not email_can_be_used_as_mailbox(new_email): flash( "You cannot use this email address as your personal inbox.", "error", ) new_email_valid = False # a pending email change with the same email exists from another user elif EmailChange.get_by(new_email=new_email): other_email_change: EmailChange = EmailChange.get_by( new_email=new_email) LOG.warning( "Another user has a pending %s with the same email address. Current user:%s", other_email_change, current_user, ) if other_email_change.is_expired(): LOG.d("delete the expired email change %s", other_email_change) EmailChange.delete(other_email_change.id) db.session.commit() else: flash( "You cannot use this email address as your personal inbox.", "error", ) new_email_valid = False if new_email_valid: email_change = EmailChange.create( user_id=current_user.id, code=random_string( 60), # todo: make sure the code is unique new_email=new_email, ) db.session.commit() send_change_email_confirmation(current_user, email_change) flash( "A confirmation email is on the way, please check your inbox", "success", ) return redirect(url_for("dashboard.setting")) if request.form.get("form-name") == "update-profile": if form.validate(): profile_updated = False # update user info if form.name.data != current_user.name: current_user.name = form.name.data db.session.commit() profile_updated = True if form.profile_picture.data: file_path = random_string(30) file = File.create(user_id=current_user.id, path=file_path) s3.upload_from_bytesio( file_path, BytesIO(form.profile_picture.data.read())) db.session.flush() LOG.d("upload file %s to s3", file) current_user.profile_picture_id = file.id db.session.commit() profile_updated = True if profile_updated: flash("Your profile has been updated", "success") return redirect(url_for("dashboard.setting")) elif request.form.get("form-name") == "change-password": flash( "You are going to receive an email containing instructions to change your password", "success", ) send_reset_password_email(current_user) return redirect(url_for("dashboard.setting")) elif request.form.get("form-name") == "notification-preference": choose = request.form.get("notification") if choose == "on": current_user.notification = True else: current_user.notification = False db.session.commit() flash("Your notification preference has been updated", "success") return redirect(url_for("dashboard.setting")) elif request.form.get("form-name") == "delete-account": # Schedule delete account job LOG.warning("schedule delete account job for %s", current_user) Job.create( name=JOB_DELETE_ACCOUNT, payload={"user_id": current_user.id}, run_at=arrow.now(), commit=True, ) flash( "Your account deletion has been scheduled. " "You'll receive an email when the deletion is finished", "success", ) return redirect(url_for("dashboard.setting")) elif request.form.get("form-name") == "change-alias-generator": scheme = int(request.form.get("alias-generator-scheme")) if AliasGeneratorEnum.has_value(scheme): current_user.alias_generator = scheme db.session.commit() flash("Your preference has been updated", "success") return redirect(url_for("dashboard.setting")) elif request.form.get( "form-name") == "change-random-alias-default-domain": default_domain = request.form.get("random-alias-default-domain") if default_domain: sl_domain: SLDomain = SLDomain.get_by(domain=default_domain) if sl_domain: if sl_domain.premium_only and not current_user.is_premium( ): flash("You cannot use this domain", "error") return redirect(url_for("dashboard.setting")) current_user.default_alias_public_domain_id = sl_domain.id current_user.default_alias_custom_domain_id = None else: custom_domain = CustomDomain.get_by(domain=default_domain) if custom_domain: # sanity check if (custom_domain.user_id != current_user.id or not custom_domain.verified): LOG.exception("%s cannot use domain %s", current_user, default_domain) else: current_user.default_alias_custom_domain_id = ( custom_domain.id) current_user.default_alias_public_domain_id = None else: current_user.default_alias_custom_domain_id = None current_user.default_alias_public_domain_id = None db.session.commit() flash("Your preference has been updated", "success") return redirect(url_for("dashboard.setting")) elif request.form.get("form-name") == "change-sender-format": sender_format = int(request.form.get("sender-format")) if SenderFormatEnum.has_value(sender_format): current_user.sender_format = sender_format current_user.sender_format_updated_at = arrow.now() db.session.commit() flash("Your sender format preference has been updated", "success") db.session.commit() return redirect(url_for("dashboard.setting")) elif request.form.get("form-name") == "replace-ra": choose = request.form.get("replace-ra") if choose == "on": current_user.replace_reverse_alias = True else: current_user.replace_reverse_alias = False db.session.commit() flash("Your preference has been updated", "success") return redirect(url_for("dashboard.setting")) elif request.form.get("form-name") == "sender-in-ra": choose = request.form.get("enable") if choose == "on": current_user.include_sender_in_reverse_alias = True else: current_user.include_sender_in_reverse_alias = False db.session.commit() flash("Your preference has been updated", "success") return redirect(url_for("dashboard.setting")) elif request.form.get("form-name") == "export-data": return redirect(url_for("api.export_data")) elif request.form.get("form-name") == "export-alias": return redirect(url_for("api.export_aliases")) manual_sub = ManualSubscription.get_by(user_id=current_user.id) apple_sub = AppleSubscription.get_by(user_id=current_user.id) coinbase_sub = CoinbaseSubscription.get_by(user_id=current_user.id) return render_template( "dashboard/setting.html", form=form, PlanEnum=PlanEnum, SenderFormatEnum=SenderFormatEnum, promo_form=promo_form, change_email_form=change_email_form, pending_email=pending_email, AliasGeneratorEnum=AliasGeneratorEnum, manual_sub=manual_sub, apple_sub=apple_sub, coinbase_sub=coinbase_sub, FIRST_ALIAS_DOMAIN=FIRST_ALIAS_DOMAIN, )
def coupon_route(): coupon_form = CouponForm() if coupon_form.validate_on_submit(): code = coupon_form.code.data if LifetimeCoupon.get_by(code=code): LOG.d("redirect %s to lifetime page instead", current_user) flash("Redirect to the lifetime coupon page instead", "success") return redirect(url_for("dashboard.lifetime_licence")) # handle case user already has an active subscription via another channel (Paddle, Apple, etc) can_use_coupon = True if current_user.lifetime: can_use_coupon = False sub: Subscription = current_user.get_subscription() if sub: can_use_coupon = False apple_sub: AppleSubscription = AppleSubscription.get_by( user_id=current_user.id) if apple_sub and apple_sub.is_valid(): can_use_coupon = False coinbase_subscription: CoinbaseSubscription = CoinbaseSubscription.get_by( user_id=current_user.id) if coinbase_subscription and coinbase_subscription.is_active(): can_use_coupon = False if coupon_form.validate_on_submit(): code = coupon_form.code.data coupon: Coupon = Coupon.get_by(code=code) if coupon and not coupon.used: if coupon.expires_date and coupon.expires_date < arrow.now(): flash( f"The coupon was expired on {coupon.expires_date.humanize()}", "error", ) return redirect(request.url) coupon.used_by_user_id = current_user.id coupon.used = True Session.commit() manual_sub: ManualSubscription = ManualSubscription.get_by( user_id=current_user.id) if manual_sub: # renew existing subscription if manual_sub.end_at > arrow.now(): manual_sub.end_at = manual_sub.end_at.shift( years=coupon.nb_year) else: manual_sub.end_at = arrow.now().shift(years=coupon.nb_year, days=1) Session.commit() flash( f"Your current subscription is extended to {manual_sub.end_at.humanize()}", "success", ) else: ManualSubscription.create( user_id=current_user.id, end_at=arrow.now().shift(years=coupon.nb_year, days=1), comment="using coupon code", is_giveaway=coupon.is_giveaway, commit=True, ) flash( f"Your account has been upgraded to Premium, thanks for your support!", "success", ) # notify admin if coupon.is_giveaway: subject = f"User {current_user} applies a (free) coupon" else: subject = f"User {current_user} applies a (paid, {coupon.comment or ''}) coupon" send_email( ADMIN_EMAIL, subject=subject, plaintext="", html="", ) return redirect(url_for("dashboard.index")) else: flash(f"Code *{code}* expired or invalid", "warning") return render_template( "dashboard/coupon.html", coupon_form=coupon_form, PADDLE_VENDOR_ID=PADDLE_VENDOR_ID, PADDLE_COUPON_ID=PADDLE_COUPON_ID, can_use_coupon=can_use_coupon, # a coupon is only valid until this date # this is to avoid using the coupon to renew an account forever max_coupon_date=arrow.now().shift(years=1, days=-1), )
def setting(): form = SettingForm() promo_form = PromoCodeForm() change_email_form = ChangeEmailForm() email_change = EmailChange.get_by(user_id=current_user.id) if email_change: pending_email = email_change.new_email else: pending_email = None if request.method == "POST": if request.form.get("form-name") == "update-email": if change_email_form.validate(): # whether user can proceed with the email update new_email_valid = True if ( sanitize_email(change_email_form.email.data) != current_user.email and not pending_email ): new_email = sanitize_email(change_email_form.email.data) # check if this email is not already used if personal_email_already_used(new_email) or Alias.get_by( email=new_email ): flash(f"Email {new_email} already used", "error") new_email_valid = False elif not email_can_be_used_as_mailbox(new_email): flash( "You cannot use this email address as your personal inbox.", "error", ) new_email_valid = False # a pending email change with the same email exists from another user elif EmailChange.get_by(new_email=new_email): other_email_change: EmailChange = EmailChange.get_by( new_email=new_email ) LOG.w( "Another user has a pending %s with the same email address. Current user:%s", other_email_change, current_user, ) if other_email_change.is_expired(): LOG.d( "delete the expired email change %s", other_email_change ) EmailChange.delete(other_email_change.id) Session.commit() else: flash( "You cannot use this email address as your personal inbox.", "error", ) new_email_valid = False if new_email_valid: email_change = EmailChange.create( user_id=current_user.id, code=random_string( 60 ), # todo: make sure the code is unique new_email=new_email, ) Session.commit() send_change_email_confirmation(current_user, email_change) flash( "A confirmation email is on the way, please check your inbox", "success", ) return redirect(url_for("dashboard.setting")) if request.form.get("form-name") == "update-profile": if form.validate(): profile_updated = False # update user info if form.name.data != current_user.name: current_user.name = form.name.data Session.commit() profile_updated = True if form.profile_picture.data: file_path = random_string(30) file = File.create(user_id=current_user.id, path=file_path) s3.upload_from_bytesio( file_path, BytesIO(form.profile_picture.data.read()) ) Session.flush() LOG.d("upload file %s to s3", file) current_user.profile_picture_id = file.id Session.commit() profile_updated = True if profile_updated: flash("Your profile has been updated", "success") return redirect(url_for("dashboard.setting")) elif request.form.get("form-name") == "change-password": flash( "You are going to receive an email containing instructions to change your password", "success", ) send_reset_password_email(current_user) return redirect(url_for("dashboard.setting")) elif request.form.get("form-name") == "notification-preference": choose = request.form.get("notification") if choose == "on": current_user.notification = True else: current_user.notification = False Session.commit() flash("Your notification preference has been updated", "success") return redirect(url_for("dashboard.setting")) elif request.form.get("form-name") == "change-alias-generator": scheme = int(request.form.get("alias-generator-scheme")) if AliasGeneratorEnum.has_value(scheme): current_user.alias_generator = scheme Session.commit() flash("Your preference has been updated", "success") return redirect(url_for("dashboard.setting")) elif request.form.get("form-name") == "change-random-alias-default-domain": default_domain = request.form.get("random-alias-default-domain") if default_domain: sl_domain: SLDomain = SLDomain.get_by(domain=default_domain) if sl_domain: if sl_domain.premium_only and not current_user.is_premium(): flash("You cannot use this domain", "error") return redirect(url_for("dashboard.setting")) current_user.default_alias_public_domain_id = sl_domain.id current_user.default_alias_custom_domain_id = None else: custom_domain = CustomDomain.get_by(domain=default_domain) if custom_domain: # sanity check if ( custom_domain.user_id != current_user.id or not custom_domain.verified ): LOG.w( "%s cannot use domain %s", current_user, custom_domain ) flash(f"Domain {default_domain} can't be used", "error") return redirect(request.url) else: current_user.default_alias_custom_domain_id = ( custom_domain.id ) current_user.default_alias_public_domain_id = None else: current_user.default_alias_custom_domain_id = None current_user.default_alias_public_domain_id = None Session.commit() flash("Your preference has been updated", "success") return redirect(url_for("dashboard.setting")) elif request.form.get("form-name") == "random-alias-suffix": scheme = int(request.form.get("random-alias-suffix-generator")) if AliasSuffixEnum.has_value(scheme): current_user.random_alias_suffix = scheme Session.commit() flash("Your preference has been updated", "success") return redirect(url_for("dashboard.setting")) elif request.form.get("form-name") == "change-sender-format": sender_format = int(request.form.get("sender-format")) if SenderFormatEnum.has_value(sender_format): current_user.sender_format = sender_format current_user.sender_format_updated_at = arrow.now() Session.commit() flash("Your sender format preference has been updated", "success") Session.commit() return redirect(url_for("dashboard.setting")) elif request.form.get("form-name") == "replace-ra": choose = request.form.get("replace-ra") if choose == "on": current_user.replace_reverse_alias = True else: current_user.replace_reverse_alias = False Session.commit() flash("Your preference has been updated", "success") return redirect(url_for("dashboard.setting")) elif request.form.get("form-name") == "sender-in-ra": choose = request.form.get("enable") if choose == "on": current_user.include_sender_in_reverse_alias = True else: current_user.include_sender_in_reverse_alias = False Session.commit() flash("Your preference has been updated", "success") return redirect(url_for("dashboard.setting")) elif request.form.get("form-name") == "expand-alias-info": choose = request.form.get("enable") if choose == "on": current_user.expand_alias_info = True else: current_user.expand_alias_info = False Session.commit() flash("Your preference has been updated", "success") return redirect(url_for("dashboard.setting")) elif request.form.get("form-name") == "ignore-loop-email": choose = request.form.get("enable") if choose == "on": current_user.ignore_loop_email = True else: current_user.ignore_loop_email = False Session.commit() flash("Your preference has been updated", "success") return redirect(url_for("dashboard.setting")) elif request.form.get("form-name") == "one-click-unsubscribe": choose = request.form.get("enable") if choose == "on": current_user.one_click_unsubscribe_block_sender = True else: current_user.one_click_unsubscribe_block_sender = False Session.commit() flash("Your preference has been updated", "success") return redirect(url_for("dashboard.setting")) elif request.form.get("form-name") == "include_website_in_one_click_alias": choose = request.form.get("enable") if choose == "on": current_user.include_website_in_one_click_alias = True else: current_user.include_website_in_one_click_alias = False Session.commit() flash("Your preference has been updated", "success") return redirect(url_for("dashboard.setting")) elif request.form.get("form-name") == "change-blocked-behaviour": choose = request.form.get("blocked-behaviour") if choose == str(BlockBehaviourEnum.return_2xx.value): current_user.block_behaviour = BlockBehaviourEnum.return_2xx.name elif choose == str(BlockBehaviourEnum.return_5xx.value): current_user.block_behaviour = BlockBehaviourEnum.return_5xx.name else: flash("There was an error. Please try again", "warning") return redirect(url_for("dashboard.setting")) Session.commit() flash("Your preference has been updated", "success") elif request.form.get("form-name") == "sender-header": choose = request.form.get("enable") if choose == "on": current_user.include_header_email_header = True else: current_user.include_header_email_header = False Session.commit() flash("Your preference has been updated", "success") return redirect(url_for("dashboard.setting")) elif request.form.get("form-name") == "export-data": return redirect(url_for("api.export_data")) elif request.form.get("form-name") == "export-alias": return redirect(url_for("api.export_aliases")) manual_sub = ManualSubscription.get_by(user_id=current_user.id) apple_sub = AppleSubscription.get_by(user_id=current_user.id) coinbase_sub = CoinbaseSubscription.get_by(user_id=current_user.id) return render_template( "dashboard/setting.html", form=form, PlanEnum=PlanEnum, SenderFormatEnum=SenderFormatEnum, BlockBehaviourEnum=BlockBehaviourEnum, promo_form=promo_form, change_email_form=change_email_form, pending_email=pending_email, AliasGeneratorEnum=AliasGeneratorEnum, manual_sub=manual_sub, apple_sub=apple_sub, coinbase_sub=coinbase_sub, FIRST_ALIAS_DOMAIN=FIRST_ALIAS_DOMAIN, ALIAS_RAND_SUFFIX_LENGTH=ALIAS_RANDOM_SUFFIX_LENGTH, )
def notify_manual_sub_end(): for manual_sub in ManualSubscription.all(): manual_sub: ManualSubscription need_reminder = False if arrow.now().shift(days=14) > manual_sub.end_at > arrow.now().shift( days=13): need_reminder = True elif arrow.now().shift(days=4) > manual_sub.end_at > arrow.now().shift( days=3): need_reminder = True user = manual_sub.user if user.lifetime: LOG.d("%s has a lifetime licence", user) continue paddle_sub: Subscription = user.get_subscription() if paddle_sub and not paddle_sub.cancelled: LOG.d("%s has an active Paddle subscription", user) continue if need_reminder: # user can have a (free) manual subscription but has taken a paid subscription via # Paddle, Coinbase or Apple since then if manual_sub.is_giveaway: if user.get_subscription(): LOG.d("%s has a active Paddle subscription", user) continue coinbase_subscription: CoinbaseSubscription = ( CoinbaseSubscription.get_by(user_id=user.id)) if coinbase_subscription and coinbase_subscription.is_active(): LOG.d("%s has a active Coinbase subscription", user) continue apple_sub: AppleSubscription = AppleSubscription.get_by( user_id=user.id) if apple_sub and apple_sub.is_valid(): LOG.d("%s has a active Apple subscription", user) continue LOG.d("Remind user %s that their manual sub is ending soon", user) send_email( user.email, f"Your subscription will end soon", render( "transactional/manual-subscription-end.txt", user=user, manual_sub=manual_sub, ), render( "transactional/manual-subscription-end.html", user=user, manual_sub=manual_sub, ), retries=3, ) extend_subscription_url = URL + "/dashboard/coinbase_checkout" for coinbase_subscription in CoinbaseSubscription.all(): need_reminder = False if (arrow.now().shift(days=14) > coinbase_subscription.end_at > arrow.now().shift(days=13)): need_reminder = True elif (arrow.now().shift(days=4) > coinbase_subscription.end_at > arrow.now().shift(days=3)): need_reminder = True if need_reminder: user = coinbase_subscription.user LOG.d( "Remind user %s that their coinbase subscription is ending soon", user) send_email( user.email, "Your SimpleLogin subscription will end soon", render( "transactional/coinbase/reminder-subscription.txt", coinbase_subscription=coinbase_subscription, extend_subscription_url=extend_subscription_url, ), render( "transactional/coinbase/reminder-subscription.html", coinbase_subscription=coinbase_subscription, extend_subscription_url=extend_subscription_url, ), retries=3, )