Related
NumericFormat is used with MUI TextField and prefix prop. When I start writing any number, I cannot write more than one number. If I write fast this time I can write all numbers but the prefix is gone.
All numbers inserted from the keyboard should be shown, and shouldn't be any missing numbers at the start the prefix should be visible but this is not working.
import { Observer } from "mobx-react-lite";
import { TextField, TextFieldProps } from "#mui/material";
import { AllowedNames } from "../utils/utils";
import { useDialogState } from "../states/dialog-state";
import { NumericFormat, NumericFormatProps } from "react-number-format";
import React from "react";
interface NumericFormatCustomProps {
onChange: (event: { target: { name: string; value: string } }) => void;
name: string;
}
const NumericFormatCustom = React.forwardRef<
NumericFormatProps<HTMLInputElement>,
NumericFormatCustomProps & NumericFormatProps<HTMLInputElement>
>(function NumericFormatCustom(props, ref) {
const { onChange, prefix, suffix, ...other } = props;
return (
<NumericFormat
{...other}
getInputRef={ref}
onValueChange={(values) => {
onChange({
target: {
name: props.name,
value: values.value,
},
});
}}
prefix={prefix}
suffix={suffix}
/>
);
});
function getValue(value: any, coefficient: number) {
if (value === "" || value === null || value === undefined) {
return "";
}
return ((value ?? 0) / Math.pow(10, coefficient)).toString();
}
function setValue(value: any, coefficient: number, decimalPlaces: number) {
if (value === "" || value === null || value === undefined) {
return null;
}
return (
(Math.round(parseFloat(value) * Math.pow(10, decimalPlaces)) /
Math.pow(10, decimalPlaces)) *
Math.pow(10, coefficient)
);
}
export type FormNumberFieldProps<T extends object> = Omit<
TextFieldProps,
"defaultValue" | "type"
> & {
dataKey: AllowedNames<T, number>;
prefix?: string;
suffix?: string;
decimalPlaces?: number;
coefficient?: number;
thousandSeparator?: boolean;
min?: number;
max?: number;
};
export default function FormNumberField<T extends object>({
dataKey,
prefix = "$ ",
suffix = "",
decimalPlaces = 0,
coefficient = 0,
thousandSeparator = true,
min,
max,
...rest
}: FormNumberFieldProps<T>) {
const [internalError, setInternalError] = React.useState("");
const stateProvider = useDialogState<T>();
const state = stateProvider.state;
return (
<Observer>
{() => {
const value = getValue(state.getValue(dataKey) as any, coefficient);
const checkMinMax = () => {
const numberValue = parseInt(value, 10);
if (typeof min === "number") {
if (numberValue < min) {
setInternalError(`The value should be more than ${min}`);
return;
}
}
if (typeof max === "number") {
if (numberValue > max) {
setInternalError(`The value should be less than ${max}`);
return;
}
}
setInternalError("");
};
return (
<TextField
{...rest}
fullWidth
inputMode="numeric"
onBlur={checkMinMax}
value={value}
onChange={(e) => {
state.setValue(
dataKey,
setValue(e.target.value, coefficient, decimalPlaces) as any,
);
}}
error={!!internalError || !!state.errorOf(dataKey)}
helperText={internalError || state.errorOf(dataKey)}
InputLabelProps={{ shrink: true }}
InputProps={{
inputComponent: NumericFormatCustom as any,
inputProps: {
prefix,
suffix,
thousandSeparator,
},
}}
/>
);
}}
</Observer>
);
}
import { Observer } from "mobx-react-lite";
import { TextField, TextFieldProps } from "#mui/material";
import { AllowedNames } from "../utils/utils";
import { useDialogState } from "../states/dialog-state";
import { NumericFormat, NumericFormatProps } from "react-number-format";
import React from "react";
interface NumericFormatCustomProps {
onChange: (event: { target: { name: string; value: string } }) => void;
name: string;
}
const NumericFormatCustom = React.forwardRef<
NumericFormatProps<HTMLInputElement>,
NumericFormatCustomProps & NumericFormatProps<HTMLInputElement>
>(function NumericFormatCustom(props, ref) {
const { onChange, prefix, suffix, ...other } = props;
return (
<NumericFormat
{...other}
getInputRef={ref}
onValueChange={(values) => {
onChange({
target: {
name: props.name,
value: values.value,
},
});
}}
prefix={prefix}
suffix={suffix}
/>
);
});
function getValue(value: any, coefficient: number) {
if (value === "" || value === null || value === undefined) {
return "";
}
return ((value ?? 0) / Math.pow(10, coefficient)).toString();
}
function setValue(value: any, coefficient: number, decimalPlaces: number) {
if (value === "" || value === null || value === undefined) {
return null;
}
return (
(Math.round(parseFloat(value) * Math.pow(10, decimalPlaces)) /
Math.pow(10, decimalPlaces)) *
Math.pow(10, coefficient)
);
}
export type FormNumberFieldProps<T extends object> = Omit<
TextFieldProps,
"defaultValue" | "type"
> & {
dataKey: AllowedNames<T, number>;
prefix?: string;
suffix?: string;
decimalPlaces?: number;
coefficient?: number;
thousandSeparator?: boolean;
min?: number;
max?: number;
};
export default function FormNumberField<T extends object>({
dataKey,
prefix = "$ ",
suffix = "",
decimalPlaces = 0,
coefficient = 0,
thousandSeparator = true,
min,
max,
...rest
}: FormNumberFieldProps<T>) {
const [internalError, setInternalError] = React.useState("");
const stateProvider = useDialogState<T>();
const state = stateProvider.state;
// I moved here
const value = getValue(state.getValue(dataKey) as any, coefficient);
return (
<Observer>
{() => {
const checkMinMax = () => {
const numberValue = parseInt(value, 10);
if (typeof min === "number") {
if (numberValue < min) {
setInternalError(`The value should be more than ${min}`);
return;
}
}
if (typeof max === "number") {
if (numberValue > max) {
setInternalError(`The value should be less than ${max}`);
return;
}
}
setInternalError("");
};
return (
<TextField
{...rest}
fullWidth
inputMode="numeric"
onBlur={checkMinMax}
value={value}
onChange={(e) => {
state.setValue(
dataKey,
setValue(e.target.value, coefficient, decimalPlaces) as any,
);
}}
error={!!internalError || !!state.errorOf(dataKey)}
helperText={internalError || state.errorOf(dataKey)}
InputLabelProps={{ shrink: true }}
InputProps={{
inputComponent: NumericFormatCustom as any,
inputProps: {
prefix,
suffix,
thousandSeparator,
},
}}
/>
);
}}
</Observer>
);
}
In the view whose code you see below I have a button that calls the searchView function. It displays an alert and in it we enter the number by which we are looking for an object and this object is returned by the getSong function. Up to this point everything is working fine. The problem I have encountered is that it now calls view and passes this found object, but the view does not refresh. I must have made some mistake that I can't locate or that I have forgotten. I would appreciate some help with this.
DetailView:
import CoreData
import SwiftUI
struct DetailView: View {
#State var song : Song
#State var isSelected: Bool
var body: some View {
VStack{
Text(song.content ?? "No content")
.padding()
Spacer()
}
.navigationBarTitle("\(song.number). \(song.title ?? "No title")", displayMode: .inline)
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
HStack{
Button(action: {
song.favorite.toggle()
isSelected=song.favorite
}) {
Image(systemName: "heart.fill")
.foregroundColor(isSelected ? .red : .blue)
}
Button(action: {
searchView()
}) {
Image(systemName: "magnifyingglass")
}
}
}
}
}
func searchView(){
let search = UIAlertController(title: "Go to the song", message: "Enter the number of the song you want to jump to.", preferredStyle: .alert)
search.addTextField {(number) in
number.placeholder = "Song number"
}
let go = UIAlertAction(title: "Go", style: .default) { (_) in
let songFound = PersistenceController.shared.getSong(number: (search.textFields![0].text)!)
DetailView(song: songFound!, isSelected: songFound!.favorite)
}
let cancel = UIAlertAction(title: "Cancel", style: .destructive) { (_) in
print("Cancel")
}
search.addAction(cancel)
search.addAction(go)
UIApplication.shared.windows.first?.rootViewController?.present(search, animated: true, completion: {
})
}
}
struct SongDetail_Previews: PreviewProvider {
static let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
static var previews: some View {
let song = Song(context: moc)
song.number=0
song.title="Title"
song.content="Content"
song.author="Author"
song.favorite=false
return NavigationView {
DetailView(song: song, isSelected: song.favorite)
}
}
}
PersistenceController:
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init() {
container = NSPersistentContainer(name: "Model")
container.loadPersistentStores { (description, error) in
if let error = error {
fatalError("Error \(error.localizedDescription)")
}
}
}
func save(completion: #escaping (Error?) -> () = {_ in}) {
let context = container.viewContext
if context.hasChanges {
do {
try context.save()
completion(nil)
} catch {
completion(error)
}
}
}
func delete(_ object: NSManagedObject, completion: #escaping (Error?) -> () = {_ in}) {
let context = container.viewContext
context.delete(object)
save(completion: completion)
}
func getSong(number: String) -> Song? {
guard let songNumber = Int64(number) else { return nil }
let context = container.viewContext
let request = NSFetchRequest<Song>(entityName: "Song")
request.predicate = NSPredicate(format: "number == %ld", songNumber)
do {
return try context.fetch(request).first
} catch {
print(error)
return nil
}
}
}
I'm trying to complete a paypal transaction using paypal-rest-sdk, everything is set up and working, however, I need to get the clientId back from paypal in the success route in order to save it in my client_feature_payment model. I found that we can set a "custom" field where we can set anything and that'll be sent back by paypal but this feautre is available in classic paypal sdk only and not available in rest-sdk one.
Is there any workaround for this?
//Paypal objects and methods from rest-sdk:
client_page: {
args: {
clientId: {
type: GraphQLString
}
},
type: ClientType,
resolve: async (_, args) => {
if (args.clientId) {
let clientMongoId = fromGlobalId(args.clientId).id;
let client = await Client.queryOne("id")
.eq(clientMongoId)
.exec();
let clientName = client.name;
let clientSecret = client.secret;
let company = await Company.queryOne("id")
.eq(client.companyId)
.exec();
let companyName = company.name;
let service = await Service.queryOne("id")
.eq(client.serviceId)
.exec();
let serviceName = service.name;
let clientFeature = await ClientFeature.query("clientId")
.eq(clientMongoId)
.exec();
let totalFeatures = [];
let clientFeatureId = [];
for (let i = 0; i < clientFeature.length; i++) {
clientFeatureId.unshift(clientFeature[i].id);
let feature = await Feature.query("id")
.eq(clientFeature[i].featureId)
.exec();
let newFeature;
feature.map(
feature =>
(newFeature = [
feature.name,
feature.cost,
feature.trial,
feature.frequency
])
);
totalFeatures.unshift(newFeature);
}
let trial, freq;
let cost = [];
totalFeatures.map(item => {
if (item[2] && item[3]) {
trial = item[2];
freq = item[3];
}
cost.unshift(item[1]);
});
const finalCost = cost.reduce((accumulator, currentValue) => {
return accumulator + currentValue;
}, 0);
let paypalFreq;
let frequencyInterval;
var isoDate = new Date(Date.now() + 1 * 60 * 1000);
switch (freq) {
case "bi-weekly":
paypalFreq = "DAY";
frequencyInterval = "7";
break;
case "monthly":
paypalFreq = "MONTH";
frequencyInterval = "1";
break;
case "3 months":
paypalFreq = "MONTH";
frequencyInterval = "3";
break;
case "6 months":
paypalFreq = "MONTH";
frequencyInterval = "6";
break;
case "1 year":
paypalFreq = "YEAR";
frequencyInterval = "1";
break;
default:
break;
}
var billingPlanAttributes = {
description:
"Create Plan for Trial & Frequency based payment for features and services used by customer",
merchant_preferences: {
auto_bill_amount: "yes",
cancel_url: "http://localhost:3000/cancel",
initial_fail_amount_action: "continue",
max_fail_attempts: "1",
return_url: "http://localhost:3000/success",
setup_fee: {
currency: "USD",
value: "0"
}
},
name: "Client Services & Features Charge",
payment_definitions: [
{
amount: {
currency: "USD",
value: finalCost
},
cycles: "0",
frequency: paypalFreq,
frequency_interval: frequencyInterval,
name: "Regular 1",
type: "REGULAR"
},
{
amount: {
currency: "USD",
value: "0"
},
cycles: "1",
frequency: "DAY",
frequency_interval: trial,
name: "Trial 1",
type: "TRIAL"
}
],
type: "INFINITE"
};
var billingPlanUpdateAttributes = [
{
op: "replace",
path: "/",
value: {
state: "ACTIVE"
}
}
];
var billingAgreementAttr = {
name: "Fast Speed Agreement",
description: "Agreement for Fast Speed Plan",
start_date: isoDate,
plan: {
id: "P-0NJ10521L3680291SOAQIVTQ"
},
payer: {
payment_method: "paypal",
payer_info: {
payer_id: clientMongoId
}
},
shipping_address: {
line1: "StayBr111idge Suites",
line2: "Cro12ok Street",
city: "San Jose",
state: "CA",
postal_code: "95112",
country_code: "US"
}
};
// Create the billing plan
let billingPlan = await new Promise((resolve, reject) => {
paypal.billingPlan.create(
billingPlanAttributes,
(error, billingPlan) => {
if (error) {
throw error;
} else {
resolve(billingPlan);
}
}
);
});
// let billingPlan = await billingPlanPromise;
// Activate the plan by changing status to Active
let billingAgreementAttributes = await new Promise(
(resolve, reject) => {
paypal.billingPlan.update(
billingPlan.id,
billingPlanUpdateAttributes,
(error, response) => {
if (error) {
throw error;
} else {
billingAgreementAttr.plan.id = billingPlan.id;
resolve(billingAgreementAttr);
}
}
);
}
);
// Use activated billing plan to create agreement
let approval_url = await new Promise((resolve, reject) => {
paypal.billingAgreement.create(
billingAgreementAttributes,
(error, billingAgreement) => {
if (error) {
throw error;
} else {
for (
var index = 0;
index < billingAgreement.links.length;
index++
) {
if (billingAgreement.links[index].rel === "approval_url") {
var approval_url = billingAgreement.links[index].href;
let newApprovalUrl =
approval_url + `&custom=${clientFeatureId}`;
resolve(newApprovalUrl);
// See billing_agreements/execute.js to see example for executing agreement
// after you have payment token
}
}
}
}
);
});
let data = {
companyId: companyName,
serviceId: serviceName,
name: clientName,
secret: clientSecret,
features: totalFeatures,
endpoint: approval_url
};
return Object.assign(data);
}
}
},
The success route:
app.get("/success", (req, res) => {
console.log("This is response", res);
let paymentToken = req.query.token;
paypal.billingAgreement.execute(paymentToken, {}, function(
error,
billingAgreement
) {
if (error) {
throw error;
} else {
console.log("Billing agreement", billingAgreement);
let date = billingAgreement.start_date;
let amountString =
billingAgreement.plan.payment_definitions[1].amount.value;
let trial =
billingAgreement.plan.payment_definitions[0].frequency_interval;
let frequencyInterval =
billingAgreement.plan.payment_definitions[1].frequency_interval;
let frequency = billingAgreement.plan.payment_definitions[1].frequency;
let totalFrequency = frequencyInterval + " " + frequency;
let period = [trial, totalFrequency];
let amount = parseInt(amountString);
try {
Payment.create({
id: uuidv1(),
date: date,
amount: amount,
period: period
});
} catch (err) {
throw new Error(err);
}
res.render("index");
}
});
});
My implementation looks very different, but I can output any payment details. My payment.execute() looks like so:
const express = require("express");
const paypal = require("paypal-rest-sdk");
const app = express;
paypal.configure({
mode: "sandbox", //sandbox or live
client_id:
"...",
client_secret:
"..."
});
app.set("view engine", "ejs");
app.get("/", (req, res) => res.render("index"));
app.post("/pay", (req, res) => {
const create_payment_json = {
intent: "sale",
payer: {
payment_method: "paypal"
},
redirect_urls: {
return_url: "http://localhost:3000/success",
cancel_url: "http://localhost:3000/cancel"
},
transactions: [
{
item_list: {
items: [
{
name: "Item",
sku: "001",
price: "3.33",
currency: "USD",
quantity: 1
}
]
},
amount: {
currency: "USD",
total: "3.33"
},
description:
"Hope this helps."
}
]
};
paypal.payment.create(create_payment_json, function(error, payment) {
if (error) {
throw error;
} else {
// console.log("Create Payment Response");
// console.log(payment.id);
// res.send('test')
for (let i = 0; i < payment.links.length; i++) {
if (payment.links[i].rel === "approval_url") {
res.redirect(payment.links[i].href);
}
}
}
});
});
app.get("/success", (req, res) => {
const payerId = req.query.PayerID;
const paymentId = req.query.paymentId;
const execute_payment_json = {
payer_id: payerId,
transactions: [
{
amount: {
currency: "USD",
total: "3.33"
}
}
]
};
paypal.payment.execute(paymentId, execute_payment_json, function(
error,
payment
) {
if (error) {
console.log(error.response);
throw error;
} else {
console.log(JSON.stringify(payment));
}
});
});
app.get("/cancel", (req, res) => {
res.send("Cancelled");
});
app.listen(3000, () => console.log("Server Started"));
I'm building an angular node app and am doing a http post request to signup a user. Its a chain of observables that gets the users social login information, signs ups the user, then on success emails the user. In dev mode everything works perfect, in prod mode, im getting a 404 not found. I also want to note that in development mode, some of my function calls in the observables on success are not being called. Its acting very strange and cannot figure out what I am doing wrong.
Here is my route controller
module.exports = {
signup: function signup(req, res) {
return User.create({
email: (req.body.email).toLowerCase(),
image: req.body.image,
name: req.body.name,
provider: req.body.provider || 'rent',
uid: req.body.uid || null
}).then(function (user) {
return res.status(200).json({
title: "User signed up successfully",
obj: user
});
}).catch(function (error) {
console.log(error);
return res.status(400).json({
title: 'There was an error signing up!',
error: error
});
});
}
};
and route
router.post('/signup', function(req,res,next) {
return usersController.signup(req,res);
});
My service
#Injectable()
export class UserService {
private devUrl = 'http://localhost:3000/user';
private url = '/user';
sub: any;
public user: User;
constructor(
private authS: AuthService,
private http: HttpClient) {
}
signup(user: User) {
return this.http.post(this.url + '/signup', user);
}
auth(provider: string) {
return this.sub = this.authS.login(provider);
}
logout() {
this.authS.logout()
.subscribe(value => {
console.log(value);
}, error => console.log(error))
}
getUser() {
return this.user;
}
}
and my component logic for signing up using social buttons
onAuth(provider: string){
this.checkmark = true;
this.uis.onSignupComplete();
setTimeout(() => {
this.checkmark = false;
this.thankyou = true;
}, 2500);
this.userService.auth(provider)
.subscribe(user => {
this.user = {
email: user['email'],
image: user['image'],
name: user['name'],
provider: user['provider'],
uid: user['uid']
};
this.userService.signup(this.user)
.subscribe(user => {
this.randomNum = Math.floor(Math.random() * 2);
let userEmail = this.user.email;
let subject = 'Welcome to the rent community';
let html = '';
if (this.randomNum === 1) {
html = this.contactS.getAutoEmail1();
} else if (this.randomNum === 0) {
html = this.contactS.getAutoEmail0();
}
let email = new Email(subject, html, userEmail);
this.contactS.setEmail(email)
.subscribe(data => {
}, response => {
// if (response.error['error'].errors[0].message === 'email must be unique') {
// this.uis.onSetError('Email is already used');
// this.uis.onError();
// setTimeout(() => {
// this.uis.onErrorOff();
// }, 2500);
// } else {
// this.uis.onSetError('There was an error');
// this.uis.onError();
// setTimeout(() => {
// this.uis.onErrorOff();
// }, 2500);
// }
console.log(response);
});
}, resp => console.log(resp));
}, response => {
console.log(response);
});
}
Here is the whole component
import {Component, DoCheck, HostListener, OnInit} from '#angular/core';
import {User} from "../user/user.model";
import {UserService} from "../user/user.service";
import {UiService} from "../ui.service";
import {ContactService} from "../contact/contact.service";
import {Email} from "../contact/email.model";
#Component({
selector: 'app-landing-page',
templateUrl: './landing-page.component.html',
styleUrls: ['./landing-page.component.css']
})
export class LandingPageComponent implements OnInit, DoCheck {
user: User = {
email: '',
image: '',
name: '',
provider: '',
uid: ''
};
signupComplete = false;
signup = false;
contact = false;
error = false;
errorStr = 'There was an error';
checkmark = false;
thankyou = false;
randomNum = Math.floor(Math.random() * 2);
#HostListener('window:keyup', ['$event'])
keyEvent(event: KeyboardEvent) {
const enter = 13;
if (event.keyCode === enter) {
// this.onSignup();
}
}
constructor(
private uis: UiService,
private contactS: ContactService,
private userService: UserService) { }
ngOnInit() {}
ngDoCheck() {
this.signup = this.uis.onGetSignup();
this.contact = this.uis.onGetContact();
this.signupComplete = this.uis.onReturnSignupComplete();
this.error = this.uis.getError();
this.errorStr = this.uis.getErrorStr();
}
onClickAction(s: string) {
this.uis.onClickAction(s);
}
onSignup() {
this.user.provider = 'rent';
this.user.uid = null;
this.userService.signup(this.user)
.subscribe(user => {
this.randomNum = Math.floor(Math.random() * 2);
let userEmail = this.user.email;
let subject = 'Welcome to the rent community';
let html = '';
if (this.randomNum === 1) {
html = this.contactS.getAutoEmail1();
} else if (this.randomNum === 0) {
html = this.contactS.getAutoEmail0();
}
let email = new Email(subject, html, userEmail);
this.checkmark = true;
this.uis.onSignupComplete();
setTimeout(() => {
this.checkmark = false;
this.thankyou = true;
}, 2500);
this.contactS.setEmail(email)
.subscribe(email => {
this.onReset();
}
, error => console.log(error));
}, response => {
if (response.error['error'].errors[0].message === 'email must be unique') {
this.uis.onSetError('Email is already used');
this.uis.onError();
setTimeout(() => {
this.uis.onErrorOff();
}, 2500);
console.log(response);
} else {
this.uis.onSetError('There was an error');
this.uis.onError();
setTimeout(() => {
this.uis.onErrorOff();
}, 2500);
console.log(response);
}
});
}
onAuth(provider: string){
this.checkmark = true;
this.uis.onSignupComplete();
setTimeout(() => {
this.checkmark = false;
this.thankyou = true;
}, 2500);
this.userService.auth(provider)
.subscribe(user => {
this.user = {
email: user['email'],
image: user['image'],
name: user['name'],
provider: user['provider'],
uid: user['uid']
};
this.userService.signup(this.user)
.subscribe(user => {
this.randomNum = Math.floor(Math.random() * 2);
let userEmail = this.user.email;
let subject = 'Welcome to the rent community';
let html = '';
if (this.randomNum === 1) {
html = this.contactS.getAutoEmail1();
} else if (this.randomNum === 0) {
html = this.contactS.getAutoEmail0();
}
let email = new Email(subject, html, userEmail);
this.contactS.setEmail(email)
.subscribe(data => {
}, response => {
// if (response.error['error'].errors[0].message === 'email must be unique') {
// this.uis.onSetError('Email is already used');
// this.uis.onError();
// setTimeout(() => {
// this.uis.onErrorOff();
// }, 2500);
// } else {
// this.uis.onSetError('There was an error');
// this.uis.onError();
// setTimeout(() => {
// this.uis.onErrorOff();
// }, 2500);
// }
console.log(response);
});
}, resp => console.log(resp));
}, response => {
console.log(response);
});
}
onReset() {
this.user.email = '';
this.user.image = '';
this.user.name = '';
this.user.provider = '';
this.user.uid = '';
}
errorStyle(): Object {
if (this.error) {
return {height: '50px', opacity: '1'};
} else if (!this.error) {
return {height: '0', opacity: '0'};
}
return {};
}
}
I want to mention I am using angular-2-social-login for the social logins. Unsure why it would be calling 404 if I am using the /user/signup route appropriately.
I'm confused as how I use the refresh token service. In my app there is a section with many playlists. When the user clicks on the playlist it runs this code:
func checkAuth() {
print("checking auth")
let auth = SPTAuth.defaultInstance()
//print(auth!.session.isValid())
if auth!.session == nil {
print("no auth")
if auth!.hasTokenRefreshService {
print("refresh token if session == nil")
self.renewTokenAndShowPlayer()
return
} else {
self.performSegue(withIdentifier: "LoginControllerSegue", sender: nil)
}
return
}
if auth!.session.isValid() && firstLoad {
// It's still valid, show the player.
print("valid auth")
self.showPlayer()
return
}
if auth!.hasTokenRefreshService {
print("refresh token")
self.renewTokenAndShowPlayer()
return
}
}
func renewTokenAndShowPlayer() {
SPTAuth.defaultInstance().renewSession(SPTAuth.defaultInstance().session) { error, session in
SPTAuth.defaultInstance().session = session
if error != nil {
print("Refreshing token failed.")
print("*** Error renewing session: \(error)")
self.performSegue(withIdentifier: "LoginControllerSegue", sender: nil)
return
}
self.showPlayer()
}
}
So let's say the user hasn't logged in yet and they goto the login player, then get authenticated.
Later, when they close the player and click on a different playlist, they are brought to the login screen again. Why is this?
I believe my refresh token service works, because whenever it is called after someone logs in, my server get's a /swap 200. Also, this only calls whenever someone comes back to the app (to the LoginViewController) after logging into Spotify, why is this?
Here is the code for my login page:
import UIKit
import WebKit
class LoginViewController: UIViewController, SPTStoreControllerDelegate, WebViewControllerDelegate {
#IBOutlet weak var statusLabel: UILabel!
var authViewController: UIViewController?
var firstLoad: Bool!
var Information: [String:String]?
override func viewDidLoad() {
super.viewDidLoad()
self.statusLabel.text = ""
self.firstLoad = true
let auth = SPTAuth.defaultInstance()
NotificationCenter.default.addObserver(self, selector: #selector(self.sessionUpdatedNotification), name: NSNotification.Name(rawValue: "sessionUpdated"), object: nil)
// Check if we have a token at all
if auth!.session == nil {
self.statusLabel.text = ""
return
}
// Check if it's still valid
if auth!.session.isValid() && self.firstLoad {
// It's still valid, show the player.
print("View did load, still valid, showing player")
self.showPlayer()
return
}
// Oh noes, the token has expired, if we have a token refresh service set up, we'll call tat one.
self.statusLabel.text = "Token expired."
print("Does auth have refresh service? \(auth!.hasTokenRefreshService)")
if auth!.hasTokenRefreshService {
print("trying to renew")
self.renewTokenAndShowPlayer()
return
}
// Else, just show login dialog
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
override var prefersStatusBarHidden: Bool {
return true
}
func getAuthViewController(withURL url: URL) -> UIViewController {
let webView = WebViewController(url: url)
webView.delegate = self
return UINavigationController(rootViewController: webView)
}
func sessionUpdatedNotification(_ notification: Notification) {
self.statusLabel.text = ""
let auth = SPTAuth.defaultInstance()
self.presentedViewController?.dismiss(animated: true, completion: { _ in })
if auth!.session != nil && auth!.session.isValid() {
self.statusLabel.text = ""
print("Session updated, showing player")
self.showPlayer()
}
else {
self.statusLabel.text = "Login failed."
print("*** Failed to log in")
}
}
func showPlayer() {
self.firstLoad = false
self.statusLabel.text = "Logged in."
self.Information?["SpotifyUsername"] = SPTAuth.defaultInstance().session.canonicalUsername
OperationQueue.main.addOperation {
[weak self] in
self?.performSegue(withIdentifier: "ShowPlayer", sender: self)
}
//self.performSegue(withIdentifier: "ShowPlayer", sender: nil)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowPlayer" {
if let destination = segue.destination as? PlayController {
destination.Information = self.Information
}
}
}
internal func productViewControllerDidFinish(_ viewController: SPTStoreViewController) {
self.statusLabel.text = "App Store Dismissed."
viewController.dismiss(animated: true, completion: { _ in })
}
func openLoginPage() {
self.statusLabel.text = "Logging in..."
let auth = SPTAuth.defaultInstance()
if SPTAuth.supportsApplicationAuthentication() {
self.open(url: auth!.spotifyAppAuthenticationURL())
} else {
// storyboard?.instantiateViewController(withIdentifier: <#T##String#>)
//
self.authViewController = self.getAuthViewController(withURL: SPTAuth.defaultInstance().spotifyWebAuthenticationURL())
self.definesPresentationContext = true
self.present(self.authViewController!, animated: true, completion: { _ in })
}
}
func open(url: URL) {
if #available(iOS 10, *) {
UIApplication.shared.open(url, options: [:],
completionHandler: {
(success) in
print("Open \(url): \(success)")
})
} else {
let success = UIApplication.shared.openURL(url)
print("Open \(url): \(success)")
}
}
func renewTokenAndShowPlayer() {
self.statusLabel.text = "Refreshing token..."
print("trying to renew")
SPTAuth.defaultInstance().renewSession(SPTAuth.defaultInstance().session) { error, session in
SPTAuth.defaultInstance().session = session
if error != nil {
self.statusLabel.text = "Refreshing token failed."
print("*** Error renewing session: \(error)")
return
}
print("refreshed token")
self.presentedViewController?.dismiss(animated: true, completion: { _ in })
self.showPlayer()
}
}
func webViewControllerDidFinish(_ controller: WebViewController) {
// User tapped the close button. Treat as auth error
print("UI Web view did finish")
let auth = SPTAuth.defaultInstance()
// Uncomment to turn off native/SSO/flip-flop login flow
//auth.allowNativeLogin = NO;
// Check if we have a token at all
if auth!.session == nil {
self.statusLabel.text = ""
return
}
// Check if it's still valid
if auth!.session.isValid() && self.firstLoad {
// It's still valid, show the player.
print("Still valid, showing player")
self.showPlayer()
return
}
}
#IBAction func loginButtonWasPressed(_ sender: SPTConnectButton) {
self.openLoginPage()
}
#IBAction func showSpotifyAppStoreClicked(_ sender: UIButton) {
self.statusLabel.text = "Presenting App Store..."
let storeVC = SPTStoreViewController(campaignToken: "your_campaign_token", store: self)
self.present(storeVC!, animated: true, completion: { _ in })
}
#IBAction func clearCookiesClicked(_ sender: UIButton) {
let storage = HTTPCookieStorage.shared
for cookie: HTTPCookie in storage.cookies! {
if (cookie.domain as NSString).range(of: "spotify.").length > 0 || (cookie.domain as NSString).range(of: "facebook.").length > 0 {
storage.deleteCookie(cookie)
}
}
UserDefaults.standard.synchronize()
self.statusLabel.text! = "Cookies cleared."
}
#IBAction func dismissViewController () {
self.dismiss(animated: true, completion: {})
}
}
And here is my node.js code:
var spotifyEndpoint = 'https://accounts.spotify.com/api/token';
/**
* Swap endpoint
*
* Uses an authentication code on req.body to request access and
* refresh tokens. Refresh token is encrypted for safe storage.
*/
app.post('/swap', function (req, res, next) {
var formData = {
grant_type : 'authorization_code',
redirect_uri : clientCallback,
code : req.body.code
},
options = {
uri : url.parse(spotifyEndpoint),
headers : {
'Authorization' : authorizationHeader
},
form : formData,
method : 'POST',
json : true
};
console.log("Options" + options);
request(options, function (error, response, body) {
if (response.statusCode === 200) {
body.refresh_token = encrpytion.encrypt(body.refresh_token);
} else {
console.log("error swapping: " + error);
}
res.status(response.statusCode);
res.json(body);
});
});
app.post('/refresh', function (req, res, next) {
if (!req.body.refresh_token) {
res.status(400).json({ error : 'Refresh token is missing from body' });
return;
}
var refreshToken = encrpytion.decrypt(req.body.refresh_token),
formData = {
grant_type : 'refresh_token',
refresh_token : refreshToken
},
options = {
uri : url.parse(spotifyEndpoint),
headers : {
'Authorization' : authorizationHeader
},
form : formData,
method : 'POST',
json : true
};
request(options, function (error, response, body) {
if (response.statusCode === 200 && !!body.refresh_token) {
body.refresh_token = encrpytion.encrypt(body.refresh_token);
}
res.status(response.statusCode);
res.json(body);
});
});
The LoginViewController is only skipped after I log in once, are shown the player, quit the app, then start the app again and click on a song. Why is this?
(Note, this is a error I get from refreshing token: *** Error renewing session: Optional(Error Domain=com.spotify.auth Code=400 "No refresh token available in the session!" UserInfo={NSLocalizedDescription=No refresh token available in the session!}))
I got my refresh code from [this] project. Is Heroku necessary?