Integrate Firebase authentication in an AngularDart app - firebase

I can not find a clear and complete explanation on how to integrate Firebase authentication (Google, e-mail, phone, ...) into an AngularDart (version 5) app.
Can you give it to me? Or, can you tell me where to find it?
Thank you!

firebase_dart_ui works for me. The repository contains an example, too.

Likewise. I spent a long time trying to find something...anything. While new to Angular Dart these examples that actually worked where a good find. The repo has examples for simple auth + phone and storage etc.
The md talks a little about what you need to setup in the Firebase console, you'll need to allow your host access for the Firebase generated keys in cloud console as well.
As Gazihan mentioned, the firebase_dart_ui package will do signing in with Google etc if you want to use Firebase UI, agreed, it's hard to follow in terms of finding steps. I had to change dev dependencies to get it to working but it did:
dev_dependencies:
build_runner: ^1.7.0
build_test: ^0.10.8
build_web_compilers: ^2.4.0
The following user/pwd signin example from the FirebaseDart repo is more straight forward and could help.
First, get your App details from your projects Firebase settings page.
index.dart
import 'dart:convert';
import 'dart:html';
import 'package:firebase/firebase.dart' as fb;
main() async {
try {
fb.initializeApp(
apiKey: "your api key",
authDomain: "your auth domain",
databaseURL: "your database url",
storageBucket: "your storage bucket");
AuthApp();
} on fb.FirebaseJsNotLoadedException catch (e) {
print(e);
}
}
class AuthApp {
final fb.Auth auth;
final FormElement registerForm;
final InputElement email, password;
final AnchorElement logout;
final TableElement authInfo;
final ParagraphElement error;
final SelectElement persistenceState, verifyEmailLanguage;
final ButtonElement verifyEmail;
final DivElement registeredUser, verifyEmailContainer;
AuthApp()
: this.auth = fb.auth(),
this.email = querySelector("#email"),
this.password = querySelector("#password"),
this.authInfo = querySelector("#auth_info"),
this.error = querySelector("#register_form p"),
this.logout = querySelector("#logout_btn"),
this.registerForm = querySelector("#register_form"),
this.persistenceState = querySelector("#persistent_state"),
this.verifyEmail = querySelector('#verify_email'),
this.verifyEmailLanguage = querySelector('#verify_email_language'),
this.registeredUser = querySelector("#registered_user"),
this.verifyEmailContainer = querySelector("#verify_email_container") {
logout.onClick.listen((e) {
e.preventDefault();
auth.signOut();
});
this.registerForm.onSubmit.listen((e) {
e.preventDefault();
var emailValue = email.value.trim();
var passwordvalue = password.value.trim();
_registerUser(emailValue, passwordvalue);
});
// After opening
if (auth.currentUser != null) {
_setLayout(auth.currentUser);
}
// When auth state changes
auth.onAuthStateChanged.listen(_setLayout);
verifyEmail.onClick.listen((e) async {
verifyEmail.disabled = true;
verifyEmail.text = 'Sending verification email...';
try {
// you will get the verification email in selected language
auth.languageCode = _getSelectedValue(verifyEmailLanguage);
// url should be any authorized domain in your console - we use here,
// for this example, authDomain because it is whitelisted by default
// More info: https://firebase.google.com/docs/auth/web/passing-state-in-email-actions
await auth.currentUser.sendEmailVerification(
fb.ActionCodeSettings(url: "followmyvoyage.firebaseapp.com"));
verifyEmail.text = 'Verification email sent!';
} catch (e) {
verifyEmail
..text = e.toString()
..style.color = 'red';
}
});
}
_registerUser(String email, String password) async {
if (email.isNotEmpty && password.isNotEmpty) {
var trySignin = false;
try {
// Modifies persistence state. More info: https://firebase.google.com/docs/auth/web/auth-state-persistence
var selectedPersistence = _getSelectedValue(persistenceState);
await auth.setPersistence(selectedPersistence);
await auth.createUserWithEmailAndPassword(email, password);
} on fb.FirebaseError catch (e) {
if (e.code == "auth/email-already-in-use") {
trySignin = true;
}
} catch (e) {
error.text = e.toString();
}
if (trySignin) {
try {
await auth.signInWithEmailAndPassword(email, password);
} catch (e) {
error.text = e.toString();
}
}
} else {
error.text = "Please fill correct e-mail and password.";
}
}
String _getSelectedValue(SelectElement element) =>
element.options[element.selectedIndex].value;
void _setLayout(fb.User user) {
if (user != null) {
registerForm.style.display = "none";
registeredUser.style.display = "block";
email.value = "";
password.value = "";
error.text = "";
var data = <String, dynamic>{
'email': user.email,
'emailVerified': user.emailVerified,
'isAnonymous': user.isAnonymous,
'metadata.creationTime': user.metadata.creationTime,
'metadata.lastSignInTime': user.metadata.lastSignInTime
};
data.forEach((k, v) {
if (v != null) {
var row = authInfo.addRow();
row.addCell()
..text = k
..classes.add('header');
row.addCell().text = "$v";
}
});
print('User.toJson:');
print(const JsonEncoder.withIndent(' ').convert(user));
verifyEmailContainer.style.display =
user.emailVerified ? "none" : "block";
} else {
registerForm.style.display = "block";
registeredUser.style.display = "none";
authInfo.children.clear();
}
}
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Auth demo</title>
<style type="text/css">
#register_form p {
color: red;
}
#auth_info td {
padding: 1px;
}
#auth_info td.header {
font-weight: bold;
text-align: right;
}
</style>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
</head>
<body>
<div class="container">
<h1>Auth demo</h1>
<form id="register_form" style="display: none">
<h2>Register</h2>
<p></p>
<label for="persistent_state">State persistence type</label>
<select id="persistent_state">
<option value="local">LOCAL</option>
<option value="session">SESSION</option>
<option value="none">NONE</option>
</select>
<br>
<input type="email" placeholder="Your E-mail" id="email">
<input type="password" placeholder="Your Password" id="password">
<input type="submit" value="Register me">
</form>
<div id="registered_user" style="display: none">
<table id="auth_info">
</table>
<div id="verify_email_container">
<h2>Send verify email</h2>
<label for="verify_email_language">Email language</label>
<select id="verify_email_language">
<option value="en">English</option>
<option value="cs">Czech</option>
<option value="de">German</option>
<option value="fr">French</option>
</select>
<button id="verify_email">Verify email</button>
</div>
Log out
</div>
</div>
<script src="https://www.gstatic.com/firebasejs/6.6.1/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/6.6.1/firebase-auth.js"></script>
<script defer src="index.dart.js" type="application/javascript"></script>
</body>
</html>
pubspec.yaml
dependencies:
firebase: ^5.0.0
http: '>=0.11.3 <0.13.0'
js: ^0.6.0
This answer is a bit all over the place, between the two examples talked about hopefully it's useful.

Related

DevExpress Web Dashboard in PiranhaCMS

I am currently working on a .NET Core application based on a CMS Framework named PiranhaCMS. This framework allows the definition of configurable "Blocks", basically widgets, that can be added by the users on their pages. The configuration page of the blocks is realized as a Vue.js component, and code is then compiled via gulp in a standard JS format (from the .vue file to the Vue.component(...) syntax) for the Piranha framework to read and render. The author of Piranha confirmed that this is the only way to define new blocks.
In one of our custom blocks, we are trying to implement a DevExpress Web Dashboard. I have tried following the steps outlined at https://docs.devexpress.com/Dashboard/401150/web-dashboard/dashboard-component-for-vue, but to no avail, since the compiler throws an Exception stating that the top-level declaration should be an export default { ... }, and not an import statement.
I came up with a workaround in which I dynamically load the required scripts and styles on the created() method of the component, and then define the dashboard in the same way I would in a classic javascript case (https://docs.devexpress.com/Dashboard/119158/web-dashboard/dashboard-control-for-javascript-applications-jquery-knockout-etc/add-web-dashboard-to-a-javascript-application);;) however, I am sure there is a more elegant solution to this problem.
Below is the code relevant to the problem. Here is the custom block itools-dashboard.vue:
<template>
<div class="form-group block-body">
<div :id="'dashboard-designer-' + uid" class="dashboard-designer">
<div :id="'dashboard_' + uid" style="height: 100%;">
</div>
</div>
<div class="row">
<div class="col-sm-6" style="padding:10px; margin-top: 0px;vertical-align: top;">
<fieldset>
<legend>Dashboard</legend>
<div class="form-group">
<label>Dashboard name</label>
<select class="form-control small" :id="'dashboard-names-' + uid" v-model="model.dashboardName.value">
<option v-for="dash in dashboardNames">{{ dash }}</option>
</select>
</div>
<div class="form-group">
<label>Update time</label>
<input class="form-control small" type="number" v-model="model.updateTime.value">
</div>
<div class="form-group">
<label>Width</label>
<input class="form-control small" type="text" v-model="model.width.value">
</div>
<div class="form-group">
<label>Height</label>
<input class="form-control small" type="text" v-model="model.height.value">
</div>
</fieldset>
</div>
<div class="col-sm-6" style="padding:10px; margin-top: 0px; background-color: #fcfcfc; border:1px dotted lightgray; vertical-align: top;">
<itools-base :model="model"></itools-base>
</div>
</div>
</div>
</template>
<script>
export default {
props: ["uid", "toolbar", "model"],
data: function () {
return {
dashboardNames: [],
dahsboardConfig: null,
updateModes: ["period", "realtime"],
basePath: "../../../../assets/",
// define all the css and js files paths
cssResources: [
"devextreme/dist/css/light.css",
"#devexpress/analytics-core/dist/css/dx-analytics.common.css",
"#devexpress/analytics-core/dist/css/dx-analytics.light.css",
"#devexpress/analytics-core/dist/css/dx-querybuilder.css",
"devexpress-dashboard/dist/css/dx-dashboard.light.min.css"
],
jsResources: [
"js/jquery/jquery-3.3.1.min.js",
"jquery-ui-dist/jquery-ui.js",
"knockout/build/output/knockout-latest.js",
"ace-builds/src-min-noconflict/ace.js",
"ace-builds/src-min-noconflict/ext-language_tools.js",
"ace-builds/src-min-noconflict/theme-dreamweaver.js",
"ace-builds/src-min-noconflict/theme-ambiance.js",
"devextreme/dist/js/dx.all.js",
"devextreme/dist/js/dx.aspnet.mvc.js",
"devextreme-aspnet-data/js/dx.aspnet.data.js",
"#devexpress/analytics-core/dist/js/dx-analytics-core.min.js",
"#devexpress/analytics-core/dist/js/dx-querybuilder.min.js",
"devexpress-dashboard/dist/js/dx-dashboard.min.js"
]
}
},
created: function () {
// dynamically add the required css
this.cssResources.forEach(x => {
let link = document.createElement("link");
link.setAttribute("href", this.basePath + x);
link.setAttribute("rel", "stylesheet");
document.head.appendChild(link);
});
// dynamically add the js files.
// It needs to be a synchronous ajax call so that the exports are visible in the code
// (eg the new DevExpress call)
this.jsResources.forEach(x => {
$.ajax({
async: false,
url: this.basePath + x,
dataType: "script"
})
});
this.model.width.value = this.model.width.value || "100%";
this.model.height.value = this.model.height.value || "300";
this.model.updateTime.value = this.model.updateTime.value || 5000;
},
mounted: function () {
var h = document.getElementById("dashboard-designer-" + this.uid).clientHeight;
DevExpress.Dashboard.ResourceManager.embedBundledResources();
var dashboardControl = new DevExpress.Dashboard.DashboardControl(document.getElementById("dashboard_" + this.uid), {
endpoint: "/api/dashboard",
workingMode: "Designer",
width: "100%",
height: "100%",
initialDashboardId: this.model.dashboardName.value,
});
dashboardControl.render();
},
beforeCreate: function () {
fetch("/api/Dashboards/GetDashboardNames")
.then(response => response.json())
.then(data => {
this.dashboardNames = data;
});
},
}
</script>
which is then compiled via gulp task to
Vue.component("itools-dashboard", {
props: ["uid", "toolbar", "model"],
data: function () {
return {
dashboardNames: [],
dahsboardConfig: null,
updateModes: ["period", "realtime"],
basePath: "../../../../assets/",
cssResources: ["devextreme/dist/css/light.css", "#devexpress/analytics-core/dist/css/dx-analytics.common.css", "#devexpress/analytics-core/dist/css/dx-analytics.light.css", "#devexpress/analytics-core/dist/css/dx-querybuilder.css", "devexpress-dashboard/dist/css/dx-dashboard.light.min.css"],
jsResources: ["js/jquery/jquery-3.3.1.min.js", "jquery-ui-dist/jquery-ui.js", "knockout/build/output/knockout-latest.js", "ace-builds/src-min-noconflict/ace.js", "ace-builds/src-min-noconflict/ext-language_tools.js", "ace-builds/src-min-noconflict/theme-dreamweaver.js", "ace-builds/src-min-noconflict/theme-ambiance.js", "devextreme/dist/js/dx.all.js", "devextreme/dist/js/dx.aspnet.mvc.js", "devextreme-aspnet-data/js/dx.aspnet.data.js", "#devexpress/analytics-core/dist/js/dx-analytics-core.min.js", "#devexpress/analytics-core/dist/js/dx-querybuilder.min.js", "devexpress-dashboard/dist/js/dx-dashboard.min.js"]
};
},
created: function () {
this.cssResources.forEach(x => {
let link = document.createElement("link");
link.setAttribute("href", this.basePath + x);
link.setAttribute("rel", "stylesheet");
document.head.appendChild(link);
});
this.jsResources.forEach(x => {
$.ajax({
async: false,
url: this.basePath + x,
dataType: "script"
});
});
this.model.width.value = this.model.width.value || "100%";
this.model.height.value = this.model.height.value || "300";
this.model.updateTime.value = this.model.updateTime.value || 5000;
},
mounted: function () {
DevExpress.Dashboard.ResourceManager.embedBundledResources();
var dashboardControl = new DevExpress.Dashboard.DashboardControl(document.getElementById("dashboard_" + this.uid), {
endpoint: "/api/dashboard",
workingMode: "Designer",
width: "100%",
height: "100%",
initialDashboardId: this.model.dashboardName.value
});
dashboardControl.render();
},
beforeCreate: function () {
fetch("/api/Dashboards/GetDashboardNames").then(response => response.json()).then(data => {
this.dashboardNames = data;
});
},
template: "\n<div class=\"form-group block-body\">\n <div :id=\"'dashboard-designer-' + uid\" class=\"dashboard-designer\">\n <div :id=\"'dashboard_' + uid\" style=\"height: 100%;\">\n </div>\n </div>\n <div class=\"row\">\n <div class=\"col-sm-6\" style=\"padding:10px; margin-top: 0px;vertical-align: top;\">\n <fieldset>\n <legend>Dashboard</legend>\n <div class=\"form-group\">\n <label>Dashboard name</label>\n <select class=\"form-control small\" :id=\"'dashboard-names-' + uid\" v-model=\"model.dashboardName.value\">\n <option v-for=\"dash in dashboardNames\">{{ dash }}</option>\n </select>\n </div>\n <div class=\"form-group\">\n <label>Update time</label>\n <input class=\"form-control small\" type=\"number\" v-model=\"model.updateTime.value\">\n </div>\n <div class=\"form-group\">\n <label>Width</label>\n <input class=\"form-control small\" type=\"text\" v-model=\"model.width.value\">\n </div>\n <div class=\"form-group\">\n <label>Height</label>\n <input class=\"form-control small\" type=\"text\" v-model=\"model.height.value\">\n </div>\n </fieldset>\n </div>\n <div class=\"col-sm-6\" style=\"padding:10px; margin-top: 0px; background-color: #fcfcfc; border:1px dotted lightgray; vertical-align: top;\">\n <itools-base :model=\"model\"></itools-base>\n </div>\n </div>\n</div>\n"
});
The gulp task responsible for the compilation, defined by Piranha, is:
var gulp = require('gulp'),
sass = require('gulp-sass'),
cssmin = require("gulp-cssmin"),
uglifyes = require('uglify-es'),
composer = require('gulp-uglify/composer'),
uglify = composer(uglifyes, console),
rename = require("gulp-rename"),
concat = require("gulp-concat");
var path = require('path'),
vueCompiler = require('vue-template-compiler'),
babel = require("#babel/core"),
babelTemplate = require("#babel/template").default,
codeFrameColumns = require('#babel/code-frame').codeFrameColumns,
babelTypes = require("#babel/types"),
through2 = require('through2');
function vueCompile() {
return through2.obj(function (file, _, callback) {
var relativeFile = path.relative(file.cwd, file.path);
console.log(relativeFile);
var ext = path.extname(file.path);
if (ext === '.vue') {
var getComponent;
getComponent = function (ast, sourceCode) {
const ta = ast.program.body[0]
if (!babelTypes.isExportDefaultDeclaration(ta)) {
var msg = 'Top level declaration in file ' + relativeFile + ' must be "export default {" \n' + codeFrameColumns(sourceCode, { start: ta.loc.start }, { highlightCode: true });
throw msg;
}
return ta.declaration;
}
var compile;
compile = function (componentName, content) {
var component = vueCompiler.parseComponent(content, []);
if (component.styles.length > 0) {
component.styles.forEach(s => {
const linesToStyle = content.substr(0, s.start).split(/\r?\n/).length;
var msg = 'WARNING: <style> tag in ' + relativeFile + ' is ignored\n' + codeFrameColumns(content, { start: { line: linesToStyle } }, { highlightCode: true });
console.warn(msg);
});
}
var ast = babel.parse(component.script.content, {
parserOpts: {
sourceFilename: file.path
}
});
var vueComponent = getComponent(ast, component.script.content);
vueComponent.properties.push(babelTypes.objectProperty(babelTypes.identifier('template'), babelTypes.stringLiteral(component.template.content)))
var wrapInComponent = babelTemplate("Vue.component(NAME, COMPONENT);");
var componentAst = wrapInComponent({
NAME: babelTypes.stringLiteral(componentName),
COMPONENT: vueComponent
})
ast.program.body = [componentAst]
babel.transformFromAst(ast, null, null, function (err, result) {
if (err) {
callback(err, null)
}
else {
file.contents = Buffer.from(result.code);
callback(null, file)
}
});
}
var componentName = path.basename(file.path, ext);
if (file.isBuffer()) {
compile(componentName, file.contents.toString());
}
else if (file.isStream()) {
var chunks = [];
file.contents.on('data', function (chunk) {
chunks.push(chunk);
});
file.contents.on('end', function () {
compile(componentName, Buffer.concat(chunks).toString());
});
}
} else {
callback(null, file);
}
});
}
var js = {
name: "itools-blocks.js",
path: "wwwroot/assets/js/blocks/*.vue"
}
//
// Compile & minimize js files
//
gulp.task("min:js", function (done) {
gulp.src(js.path, { base: "." })
.pipe(vueCompile())
.pipe(concat("wwwroot/assets/js/blocks/" + js.name))
.pipe(gulp.dest("."))
.pipe(uglify().on('error', function (e) {
console.log(e);
}))
.pipe(rename({
suffix: ".min"
}))
.pipe(gulp.dest("."));
done();
});
any kind of help is well appreciated
The gulpfile with the method “vueCompile” that you’re referring to was specifically written to suit the needs of the internal components we provide in the framework, it’s by no means a silver bullet for all Vue component compilation. However I understand your problem, before writing this code we desperately searched for existing npm-packages that would give us the functionality we needed, but this wasn’t that easy to find as we only use a subset of the features available in Vue.js
We’d be more than happy to get feedback or more information on how this could be done, so we’ll be watching this thread 👍🏼

reCAPTCHA not loading properly in XHTML document

I am writing an SHTML CSS & JS document into an XML document which uses an XHTML parser.
When I try to load the captcha into it, it briefly shows up as a white box then disappears, never to be seen again inside the renderer.
I understand that this might be caused by the scripts being loaded in the wrong order but I don't have the luxury of the async defer keywords that are recommended by google simply because the parser won't accept them.
<script src="https://www.google.com/recaptcha/api.js" type="text/javascript"></script>
<script type="text/javascript"><![CDATA[
class Token {
constructor(tokenInstance) {
this.props = tokenInstance;
this.getStatus();
this.setOnConfirm();
this.setAdditionalProps();
}
setAdditionalProps() {
this.props.claimed = false;
this.props.claimTokensMessage = "Claim free DAI, DeFi & ETH tokens";
this.props.claimedMessage = "You have already claimed DAI, DeFi & ETH tokens, stay posted for new freebies!";
this.props.serverError = "Server Error, please try again later";
this.props.success = "ETH, DeFi & DAI tokens will be sent to your address shortly!";
this.props.fillCaptchaMessage = "Please click the captcha below to receive the tokens";
}
getBaseURL() {
return "http://192.168.26.189:8080/api/"
}
getStatus() {
let suffix = "claimed?userAddress=" + this.props.ownerAddress;
fetch(this.getBaseURL() + suffix).then((res) => {
if(parseInt(res.status) === 200) {
document.getElementById("message").innerHTML = this.props.claimTokensMessage;
} else if(parseInt(res.status) === 412) {
this.props.claimed = true;
document.getElementById("message").innerHTML = this.props.claimedMessage;
} else {
document.getElementById("message").innerHTML = this.props.serverError;
}
}).catch((err) => {
document.getElementById("message").innerHTML = err;
window.onConfirm = function() { window.close() };
});
}
setOnConfirm() {
window.onConfirm = () => {
if(this.props.claimed) {
window.close();
} else {
web3.personal.sign({ data: "To receive free tokens, you must reveal your public address to a smart contract. Is that ok?" }, (err, val) => {
if(err) { throw err; }
//user completes the request by filling the captcha
document.getElementById("message").innerHTML = this.props.success;
document.getElementById("captcha").submit();
window.onConfirm = function () { window.close(); };
});
}
};
}
render() {
let suffix = "discover?userAddress=" + this.props.ownerAddress;
return`
<div class="ui container">
<div class="ui segment">
<img src="data:image/png;base64,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">
<h3 id="message"></h3>
<form action=${this.getBaseURL() + suffix} id="captcha" method="post">
<button class="g-recaptcha" data-sitekey="6LeK070UAAAAAHuzSEGgoqbuLGuJq-GRDd-LA4kH" data-callback="onSubmit" hidden>fill captcha</button>
</form>
</div>
</div>
`;
}
}
web3.tokens.dataChanged = (oldTokens, updatedTokens) => {
const currentTokenInstance = web3.tokens.data.currentInstance;
document.getElementById('root').innerHTML = new Token(currentTokenInstance).render();
};
]]></script>
<div id="root"></div>

Firebase Google sign in not working

I've made a sign in code like this:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Gmail Sign In</title>
<script src="https://www.gstatic.com/firebasejs/4.6.2/firebase.js"></script>
<script>
// Initialize Firebase
var config = {
apiKey: "AIzaSyC04no7Ge4ku9xQI3bjJknjhUK9W3UuoiE",
authDomain: "sb-sign.firebaseapp.com",
databaseURL: "https://sb-sign.firebaseio.com",
projectId: "sb-sign",
storageBucket: "sb0!-sign.appspot.com",
messagingSenderId: "431178337718"
};
firebase.initializeApp(config);
window.onload = login();
function login(){
firebase.auth().onAuthStateChanged(newLoginHappend);
function newLoginHappend(user){
if (user){
//user has signed in
app(user);
document.getElementById("SignOutButton").style = "display:block";
}
else{
document.getElementById("SignInButton").style = "display:block";
document.getElementById("ClientName").innerHTML = there! please login.;
}
function app(user){
//user.displayName
//user.email
//user.photoURL
//user.uid
document.getElementById("ClientName").innerHTML = user.displayName;
}
SignIn(){
var provider = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithRedirect(provider);
}
}
}
</script>
</head>
<body>
<h1>
Hello, <span id="ClientName"></span>
<button id="SignInButton" style="display:none" onclick='SignIn()'>Sign In</button> <button id="SignOutButton" style="display:none" onclick='firebase.auth().signOut();window.refresh();'>Sign Out</button>
</h1>
</body>
</html>
When body loads, if user is not signed in, then want to show the sign-in button (#SignInButton), hide the sign out button and show a text output saying "hello there! Please login". And when user click over that button, I want show 'em the sign in page.
On body load, if the user is already signed in, I wants to show the text, saying "hello ". And also, I want to hide the sign in button and show the sign out button.
But when I open this page, I just saw a text "Hello" in the screen. Nothing else shown. Nor sign-in button neither signout button.
Please help.
Thanks in advance.
Your javascript contain syntax error:
SyntaxError: missing ; before statement firebase.html:29:60
That means that string should be quoted like this:
document.getElementById("ClientName").innerHTML = 'there! please login.';
or like this:
document.getElementById("ClientName").innerHTML = "there! please login.";
I've found the answer my self.
I've done some code rewriting and some changes.
Now it's working properly.
Here's the new code:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Gmail Sign In</title>
<script src="https://www.gstatic.com/firebasejs/4.6.2/firebase.js"></script>
<script>
// Initialize Firebase
var config = {
apiKey: "AIzaSyC04no7Ge4ku9xQI3bjJknjhUK9W3UuoiE",
authDomain: "sb-sign.firebaseapp.com",
databaseURL: "https://sb-sign.firebaseio.com",
projectId: "sb-sign",
storageBucket: "sb-sign.appspot.com",
messagingSenderId: "431178337718"
};
firebase.initializeApp(config);
function login(){
function newLoginHappend(user){
if (user){
//user has signed in
app(user);
document.getElementById("SignOutButton").style = "display:block";
document.getElementById("SignInButton").style = "display:none";
}
else{ }
}
firebase.auth().onAuthStateChanged(newLoginHappend);
}
function login2(){
function newLoginHappend(user){
if (user){
//user has signed in
app(user);
document.getElementById("SignOutButton").style = "display:block";
}
else{
var provider = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithRedirect(provider);
}
}
firebase.auth().onAuthStateChanged(newLoginHappend);
}
function app(user){
//user.displayName
//user.email
//user.photoURL
//user.uid
document.getElementById("ClientName").innerHTML = user.displayName;
}
window.onload = login();
</script>
</head>
<body>
<h1>
Hello, <span id="ClientName"></span>
</h1>
<button id="SignInButton" style="display:block" onclick='login2()'>Sign In</button> <button id="SignOutButton" style="display:none" onclick='firebase.auth().signOut();location.reload(true);'>Sign Out</button>
</body>
</html>

Youtube-Js-Api / How can I include the request’s response in my actual webpage?

I'm working on an app with both Firebase (web app) and the youtube API. The idea is to let users share their playlists and interact.
1) First, the user must identify himself. This part works very well (firebase part)
2) Then, the user must accept some conditions (the scopes) from the youtube API.
3) The Youtube API return the result of the request.
The issue is that youtube API recreate for every item of the array the HTML structure : HTML > HEAD > BODY. Plus, the response skips my header and display the all thing in a blank page instead of reacting like an include PHP (i know this is javascript but still.. ).
I know the solution rest in the function executerequest but i can't figurate how to do it.
The code of the request is in the middle of the body but for the purpose of my post, i did separate it.
var GoogleAuth;
var SCOPE = 'https://www.googleapis.com/auth/youtube.force-ssl';
function handleClientLoad() {
// Load the API's client and auth2 modules.
// Call the initClient function after the modules load.
gapi.load('client:auth2', initClient);
}
function initClient() {
// Retrieve the discovery document for version 3 of YouTube Data API.
// In practice, your app can retrieve one or more discovery documents.
var discoveryUrl = 'https://www.googleapis.com/discovery/v1/apis/youtube/v3/rest';
// Initialize the gapi.client object, which app uses to make API requests.
// Get API key and client ID from API Console.
// 'scope' field specifies space-delimited list of access scopes.
gapi.client.init({
'apiKey': '...',
'discoveryDocs': [discoveryUrl],
'clientId': '....',
'scope': SCOPE
}).then(function () {
GoogleAuth = gapi.auth2.getAuthInstance();
// Listen for sign-in state changes.
GoogleAuth.isSignedIn.listen(updateSigninStatus);
// Handle initial sign-in state. (Determine if user is already signed in.)
var user = GoogleAuth.currentUser.get();
setSigninStatus();
// Call handleAuthClick function when user clicks on
// "Sign In/Authorize" button.
$('#sign-in-or-out-button').click(function() {
handleAuthClick();
});
$('#revoke-access-button').click(function() {
revokeAccess();
});
});
}
function handleAuthClick() {
if (GoogleAuth.isSignedIn.get()) {
// User is authorized and has clicked 'Sign out' button.
$('#sign-in-or-out-button').html('Sign out');
$('#revoke-access-button').css('display', 'inline-block');
GoogleAuth.signOut();
} else {
// User is not signed in. Start Google auth flow.
GoogleAuth.signIn();
}
}
function revokeAccess() {
GoogleAuth.disconnect();
}
function setSigninStatus(isSignedIn) {
var user = GoogleAuth.currentUser.get();
var isAuthorized = user.hasGrantedScopes(SCOPE);
if (isAuthorized) {
$('#sign-in-or-out-button').html('Sign out');
$('#revoke-access-button').css('display', 'inline-block');
$('#auth-status').html('Connecté ' +
' Granted');
defineRequest();
console.log('connecté');
} else {
$('#roomRed').html('display', 'block');
$('#sign-in-or-out-button').html('Sign In/Authorize');
$('#revoke-access-button').css('display', 'none');
$('#auth-status').html('Déconnecté' +
' Denied');
console.log('déconnecté');
}
// This helper method displays a message on the page.
}
function updateSigninStatus(isSignedIn) {
setSigninStatus();
}
function createResource(properties) {
var resource = {};
var normalizedProps = properties;
for (var p in properties) {
var value = properties[p];
if (p && p.substr(-2, 2) == '[]') {
var adjustedName = p.replace('[]', '');
if (value) {
normalizedProps[adjustedName] = value.split(',');
}
delete normalizedProps[p];
}
}
for (var p in normalizedProps) {
// Leave properties that don't have values out of inserted resource.
if (normalizedProps.hasOwnProperty(p) && normalizedProps[p]) {
var propArray = p.split('.');
var ref = resource;
for (var pa = 0; pa < propArray.length; pa++) {
var key = propArray[pa];
if (pa == propArray.length - 1) {
ref[key] = normalizedProps[p];
} else {
ref = ref[key] = ref[key] || {};
}
}
};
}
return resource;
}
function removeEmptyParams(params) {
for (var p in params) {
if (!params[p] || params[p] == 'undefined') {
delete params[p];
}
}
return params;
}
function executeRequest(request) {
request.execute(function(response) {
console.log(response);
for(var i = 0; i< response.items.length; i++){
console.log(response.items[i].player.embedHtml);
document.write(response.items[i].player.embedHtml);
}
});
}
function buildApiRequest(requestMethod, path, params, properties) {
params = removeEmptyParams(params);
var request;
if (properties) {
var resource = createResource(properties);
request = gapi.client.request({
'body': resource,
'method': requestMethod,
'path': path,
'params': params
});
} else {
request = gapi.client.request({
'method': requestMethod,
'path': path,
'params': params
});
}
executeRequest(request);
}
/***** END BOILERPLATE CODE *****/
function defineRequest() {
// See full sample for buildApiRequest() code, which is not
// specific to a particular youtube or youtube method.
buildApiRequest('GET',
'/youtube/v3/playlists',
{
'mine': 'true',
'maxResults': '25',
'part': 'snippet,contentDetails,player',
'onBehalfOfContentOwner': '',
'onBehalfOfContentOwnerChannel': ''
});
/*
buildApiRequest('GET',
'/youtube/v3/playlistItems',
{
'playlistId': "PLsvlo6Soc2pc2ZlereiehdPRhm0eKjSxI",
'maxResults': '25',
'part': 'snippet,contentDetails'
});
*/
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Mes vidéo </title>
<style>
</style>
</head>
<body>
<button id="sign-in-or-out-button"
style="margin-left: 25px">Sign In/Authorize</button>
<button id="revoke-access-button"
style="display: none; margin-left: 25px">Revoke access</button>
<div id="auth-status" style="display: inline; padding-left: 25px"></div><hr>
<div id="video-container"></div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script async defer src="https://apis.google.com/js/api.js"
onload="this.onload=function(){};handleClientLoad()"
onreadystatechange="if (this.readyState === 'complete') this.onload()">
</script>
</body>
</html>
Thank you by advance

Password Reset token is returning null, although defined

I'm utilizing useraccounts:unstyled, accounts-base, accounts-password and trying to implement a password resetting feature.
I have my route defined as such:
FlowRouter.route('/reset-password/:token', {
name: 'reset-password',
onBeforeAction: function()
Accounts._resetPasswordToken = this.params.token;
this.next();
},
action(params){
Accounts._resetPasswordToken = params.token;
mount(MainLayout, {
content: (<ForgotPassword />)
});
}
});
My template defined as such:
<template name="ForgotPasswordModal">
{{#if $not currentUser}}
<div class="forgot-modal {{$.Session.get 'nav-toggle'}}" id="{{checkState}}">
<i class="fa fa-close resetPwd"></i>
{{> atForm}}
</div>
{{/if}}
</template>
And my helper functions defined as:
if (Meteor.isClient) {
Template.ForgotPasswordModal.onCreated(function(){
if(Accounts._resetPasswordToken){
Session.set('resetPasswordToken', Accounts._resetPasswordToken);
}else{
console.log("else");
}
});
Template.ForgotPasswordModal.helpers({
checkState() {
return (AccountsTemplates.getState() == 'resetPwd') ? 'forgot-modal' : '';
}
});
Template.ForgotPasswordModal.events({
"submit .at-btn": (event)=>{
event.preventDefault();
console.log(event);
password = document.getElementById('reset-password-new-password').value;
console.log("password", password);
if(password){
Accounts.resetPassword(Session.get('resetPasswordToken'), password, (error)=>{
if(error){
console.log("error: ", error);
}else{
console.log("success");
Session.set('resetPasswordToken', null);
}
});
}
}
});
}
Upon clicking submit I get Error: Match error: Expected string, got null (unknown).
Although if I load the route up (with a valid token) and run Session.get('resetPasswordToken') the token is returned validly.
I was getting this for a few days, couldn't figure it out... then after some rearrangement it finally worked.
And you don't have to use Meteor's default route & form for resetting the password.
You're close #Sleep Deprived Bulbasaur, your route should look like this:
FlowRouter.route('/reset-password/:token', {
name: 'reset-password',
action(params){
Session.set('_resetPasswordToken', params.token);
mount(MainLayout, {
content: (<ForgotPassword />)
});
}
});
I did not need the onBeforeAction, Accounts._resetPasswordToken, or this.next(), and it works just fine and logs you in automatically.
Your template should have something like this:
if (!validateForm(password,passwordAgain)) {
console.log('Your passwords dont match');
} else if (!isValidPassword(password, passwordAgain)) {
console.log('You do not have valid passwords');
} else {
let token = Session.get('_resetPasswordToken');
Accounts.resetPassword(token, password, function(err) {
check(token, String);
if (err) {
console.log('We are sorry but something went wrong.');
} else {
console.log('Your password has been changed. Welcome back!');
}
});
}
Please try using this way:
FlowRouter.route('/#/reset-password/:token');
This is default route for reset Password.

Resources