[HIGH TONE] [MUSIC PLAYING] EMILY FORTUNA: Hello, and
welcome to "The Boring Show." I'm Emily Fortuna. And with us is a special guest. SHANNON SKIPPER:
I'm Shannon Skipper. I'm with Square. EMILY FORTUNA: So
today, we are going to be shifting our focus
to a different app. If you have ever tried the
Flutter gallery demo app, one of the demos within
that demo is called Shrine. It's a shopping app. And I pulled that out. I made it a separate repo on my
GitHub repo, which is efortuna. It's called Shrine with Square. And so basically, what
we're going to be doing is we're going to
be adding In-App Payments to that shopping app. And Shannon is going
to tell us how. SHANNON SKIPPER: Absolutely. EMILY FORTUNA: So
yeah, so first of all, here's our little Shrine app. We're not going
to log in for now. I'm just doing the basic thing. SHANNON SKIPPER: Perfect. EMILY FORTUNA: You
can click on things, add them to your shopping cart. And right now, all you
can do is clear your cart. So I think the
next obvious thing is to add a button that's
like, take my money. SHANNON SKIPPER: Pay me. EMILY FORTUNA: So I was
poking around a little bit. Shopping cart is this page here. Oh, I should actually say
one other quick thing. Also, this app is featured in
some of the material code labs. There's a four-part-- four
different Material codelabs that all help you build
out this very app as well. So it's many different ways
you can reach this app. But yes, OK, so
here, we've got-- here's our button
for clear cart. It is a fancy button. I guess, since we're-- the gross thing to do
would be to copy-paste. Let's-- SHANNON SKIPPER: Yeah. EMILY FORTUNA: Whoa,
let's not do that. How about we pull this
out into a function? SHANNON SKIPPER: Nice. EMILY FORTUNA: And then
we can have our text. Although-- yeah, so
it'll have our text and our OnPressed method. SHANNON SKIPPER: Keep it dry. EMILY FORTUNA: Yes. So I think there's
a shortcut for this, but I don't remember it. So I'm not going to do it. Pretty button-- and
we will figure out what we're passing in in
a second, return that. We need our model. OK, so pretty button-- and let's pass in model. Or wait, how-- this code base
is a little bit new to me too, so I'm trying to figure
out what's going on here. So we have this model. It's a scoped model, great. So we don't actually need
to pass it in, because-- oh, well, so we
need our context. I'm going to pass it in for
right now, because that's not the focus. And we will come back to fixing
up state management afterwards. So scoped model-- or wait, app
state model is what we want. And we will pass in model. OK, so that's that. And then let's add our text
that we want to add in. SHANNON SKIPPER: OK. EMILY FORTUNA: So string text. SHANNON SKIPPER: So the
original one says clear cart. And we'll add something
like pay or check out. EMILY FORTUNA: Yeah, exactly. So cut that. Pass in text. Why is that not-- oh. Oh, it thinks it's
constant, right? OK-- no longer constant. We were pass in clear cart. And we also want to pass
in our function for what we're going to do. Because that will change
depending on what we're doing. SHANNON SKIPPER: Nice. EMILY FORTUNA: So
we'll cut that out. And we'll say action-- function action. And we'll pass that in. Great. OK, so that's button 1. We didn't change anything. We will-- what are
you doing, Shannon? SHANNON SKIPPER: Oh, I'm just-- I'm exploring the--
I was looking-- I was just trying to figure
out what these buttons are. EMILY FORTUNA: The raise button? SHANNON SKIPPER: No, the-- checking out what state-- what state it needed, and
then, when it took the string. And then you got
ahead of me with the-- that it needs the function. EMILY FORTUNA: OK, cool. All right, so we've
got everything. Let's add our second function,
the function that does the-- brings the money. SHANNON SKIPPER: Yay. EMILY FORTUNA: So let's
wrap this in a column. SHANNON SKIPPER: OK. EMILY FORTUNA: And we can put-- we could have our Buy button
up top, and then the Clear Cart button just below it. SHANNON SKIPPER: Nice. And so if it was a
column, it doesn't need to be fixed positioning? It'll float? EMILY FORTUNA: Yeah it'll just-- we'll see. SHANNON SKIPPER: OK. EMILY FORTUNA: But I believe-- well, so yeah, this is
wrapped in a positioned. But I believe it should work. We might need to tweak it. We will find out. SHANNON SKIPPER: Nice. EMILY FORTUNA: OK, so-- nope. Why-- OK. There we go. So wrap with column. And we are going to--
let's stick it below-- or no, above. And we'll say, take my money. SHANNON SKIPPER: Nice. EMILY FORTUNA: And
nothing happens. That's awesome. SHANNON SKIPPER: Uh-oh. EMILY FORTUNA: Hmm, go away. Hot restart? Did my computer-- don't
seem to be doing anything. Let's try again. And here I thought I had
just been coding so well up until this point. And so it was just not
actually running or something. While this is
loading up, you were looking at some GitHub pages,
like documentation about that. Do you want to-- SHANNON SKIPPER: Absolutely,
I was looking at the Payments Flutter plug-in
documentation, just to see how we initialize
the Payments SDK with our application
ID, and then what we do when we click this
button, which is the payment part, where it's
going to put up, on the screen, a
pretty credit card. And we can customize that. We probably should
match it to the app. EMILY FORTUNA: Nice. SHANNON SKIPPER: And then when
the user puts in all their card details, their secrets. Instead of getting
those secrets back-- so we have to deal with
the PCI compliance, or that sort of thing-- it's going to give us
just a one-time use nonce. And so that is basically this
card in a one-time use secret key that we then will
pass to the Square API to either make the payment
or save the card on file with the customer, which is
a really more flexible way to do it. We just need to
tell the customer, do you want to save this card? And if they do, then we can go
buy more Shrine shopping cart items. EMILY FORTUNA: Yes,
more clothes and things. SHANNON SKIPPER: Make
it easy for the user. EMILY FORTUNA: Nice. OK, so this is up. We'll see if this is working. Got that. Take my money is available. SHANNON SKIPPER: Oh, nice,
so the function works. EMILY FORTUNA: I don't
know why it hung there. SHANNON SKIPPER: Just
thinking about it. EMILY FORTUNA: What's going on. MyApp isn't defined. Oh, that's the test thing. That shouldn't be a problem. Let's try making a
change somewhere else. I'm very confused. Add some exclamation points,
because what could go wrong? OK. Well, we're off
to a great start. SHANNON SKIPPER: It liked
extracting one button, it just didn't like the second. EMILY FORTUNA: It didn't like
putting in-- it didn't like hot reload is what it didn't like-- SHANNON SKIPPER: Oh. EMILY FORTUNA:
--for some reason. SHANNON SKIPPER: Hmm. EMILY FORTUNA: As we build
again, what would be-- I mean, this shouldn't
be causing problems. Going to clear our test
thing so there's no problems. OK, so we have Shrine. We've got our entry point. Part of this problem is my lack
of familiarity with this app. We're figuring
this out together. Let's see-- username. This is where I should-- I'm embarrassed
that I am not a-- I'm going to search,
on command line, our username for our login-- git grep username. Login.dart-- I'm just
testing hot reload. SHANNON SKIPPER:
I am just trying to do the non-dry
copy-paste the button just get make sure that works. EMILY FORTUNA:
It's not reloading. What's going on here? So it's not-- it's also not-- I wonder if I have some
weird setting in VS Code. Because it's not
even attempting-- like normally,
here, it would say-- it'll say something
along the hot reload. Let me try launching this
from the command line. And if-- I could also
try from Android Studio. SHANNON SKIPPER: See
if a Flutter run works? EMILY FORTUNA: Mm-hmm. I'm wondering if I just somehow
borked my Visual Studio. SHANNON SKIPPER: Oh, no EMILY FORTUNA: This
is very mysterious. OK, so we've got our
username, question mark? Save it. Do hot restart. OK, so it works from
the command line. There's something funky
in my Visual Studio Code. Maybe I'm just going
to try restarting it. And if that doesn't work, I
will go to Android Studio. SHANNON SKIPPER: Turning
it back off and on again should always work. EMILY FORTUNA: Look at you,
you've already got your-- meanwhile Shannon is
dashing ahead here. SHANNON SKIPPER: I just
copy-pasted the button. That's not fair. EMILY FORTUNA: No,
but you had the-- you had the payment flow too. SHANNON SKIPPER: Yeah. EMILY FORTUNA: OK, hold on. I will try loading
it up one more time. SHANNON SKIPPER: And I'm
trying from Android Studio, just to see. EMILY FORTUNA: OK, so username-- so you got that-- question mark. Now it works. SHANNON SKIPPER: Yay. EMILY FORTUNA: So I'm just going
to say Visual Studio Code was in a weird state, I think. So we add this. Because we're going to
spend $120 on a backpack, plus shipping. SHANNON SKIPPER: OK. EMILY FORTUNA: And
we've got two buttons. SHANNON SKIPPER: Yay. EMILY FORTUNA: All right-- SHANNON SKIPPER: Turning it
off and back on again worked. EMILY FORTUNA: All right,
so we've got our buttons. And right now
though, take my money is going to clear our cart. So what do we do Shannon? SHANNON SKIPPER: OK,
so we need to trigger the checkout the cart, or
take my money, function. Which, I guess we could put
it with the other model items, or define it wherever. But we basically need to-- EMILY FORTUNA: Yeah I can-- I should pull this out so
it's not this huge thing. Let me-- payment-- just make a payment function. And we can do what
we want with it. SHANNON SKIPPER: So I'm
looking at the Square Flutter plug-in In-App Payments
quick start docs. EMILY FORTUNA: Is that this one? SHANNON SKIPPER: Let's
see, that is that one. EMILY FORTUNA: OK. SHANNON SKIPPER: And checking
out the main functions-- actually, I think
there is a link to the methods at a glance. That's in the In-App
Payments Flutter plug-in. [INAUDIBLE] plug-in.
[? That's where I ?] get started. EMILY FORTUNA: Is it this one? SHANNON SKIPPER:
It's under master-- it's under
doc/reference.markdown. EMILY FORTUNA: There we go. It's a different page. This is the-- oh, yeah,
reference.markdown. OK, methods at a glance-- SHANNON SKIPPER:
OK, so at a glance, we have setSquareApplicationId. So we'll need to do the-- declare a future for that
and then put it somewhere where it can-- we could
do it on every pay me, but we could also
just do it once. Because it's like, if we were
changing merchants or something fancy-- but assuming we have just
one merchant, one location, or one area here, we
can just do it up front. EMILY FORTUNA: Would you-- the only-- you were
saying changing merchants. Would it be more
changing payment-- like, who's handling the
payments though? SHANNON SKIPPER: No, not really. It's more just like, if you
had, for some re-- like, let's say it was Etsy, or
you had multiple different-- EMILY FORTUNA: Sellers. SHANNON SKIPPER: --sellers
who were on the same Shrine. But since Shrine is, let's
say, one company selling-- EMILY FORTUNA: Right. Right. SHANNON SKIPPER: --we can-- I think we could do it along-- we could just call
the future along with the start card entry
flow, which is sort of the, let's take a payment. So these are the
two functions we need to call,
setSquareApplicationId and startCardEntryFlow. EMILY FORTUNA: So application
ID is kind of like a-- SHANNON SKIPPER: Initializer. EMILY FORTUNA: Yes,
but also like a-- words are hard-- the
seller ID, sort of? SHANNON SKIPPER: Totally, yeah,
it's your ID for your app. I guess you could
have multiple apps. But it's basically
like, this is Shrine, and here's our ID
in Square's API. EMILY FORTUNA: OK. SHANNON SKIPPER: And then
the startCardEntryFlow will like make magic happen. EMILY FORTUNA: OK. So string, set application-- SHANNON SKIPPER: So in this-- EMILY FORTUNA: So how do we
get a Square application ID? SHANNON SKIPPER: OK, so we-- you go into-- you
basically-- there's a link for, step forward, get
your Square application ID. EMILY FORTUNA: OK. SHANNON SKIPPER: We can
just-- we can skip this. Since we're not hitting
the back end right now, we can just put "test
application ID." EMILY FORTUNA: Oh,
OK, it'll just-- it won't error out if you're-- if you put garbage? SHANNON SKIPPER: Well, I
guess that's a good question. Maybe we should use one of
my Square application IDs. EMILY FORTUNA: I mean-- SHANNON SKIPPER: Let's
start with just, like-- EMILY FORTUNA: --we can test
it and see what happens. SHANNON SKIPPER: Yeah, let's
start with just "application ID" as the ID. EMILY FORTUNA: All right. SHANNON SKIPPER: When we
really make a payment, it will then be like, wait,
that's not an application. EMILY FORTUNA: OK, so
we need to define-- do we need to add a pubspec
and all that fun stuff? SHANNON SKIPPER: Yes. So we have-- we
should have to add-- OK, here it is. We add one dependency, which
is the Square In-App Payments plug-in. And it's square_in_app_payments. EMILY FORTUNA: I feel like I'm
on a different page than you. Oh, here we go Here we go. SHANNON SKIPPER: Are
we on the right-- are we on the same
demo, get started? There's a couple
different get started. OK. EMILY FORTUNA:
OK, so copy that-- 1.1.0. SHANNON SKIPPER:
And then the uptick is like a permissive versioning. Does that let you-- EMILY FORTUNA: Yeah,
it's basically anything up to the next major version. SHANNON SKIPPER: Gotcha. So if it was 1.9.0,
it would still-- EMILY FORTUNA: Yes. SHANNON SKIPPER: --do it. Or we could just pin it to one. EMILY FORTUNA: Yeah,
if you have nothing, it'll just be literally that. But this allows for
minor bug fixes. SHANNON SKIPPER:
Semantic versioning. EMILY FORTUNA: Yes, exactly. So we got that. OK, we're running
flutter packages get. And then we will import-- once this-- SHANNON SKIPPER:
That should be it for getting the
plug-in available. EMILY FORTUNA: So
import package square. SHANNON SKIPPER:
Looks like we're just doing even steps, so like
step 3, step 5, step 7. EMILY FORTUNA: What
are the odd steps? SHANNON SKIPPER: No odds, yeah. EMILY FORTUNA: OK,
So we got that. What did I-- I just saw red. And yes, all right, so
now we've got our garbage. SHANNON SKIPPER: Sweet. EMILY FORTUNA: OK, so now
we've initialized our app. And now we want to, as you
said, make the payment. So-- SHANNON SKIPPER:
OK, so that is on to step 7-- implement
the payment flow. So we need to import the
Square In-App Payments. EMILY FORTUNA: One
quick question-- SHANNON SKIPPER: Yes. EMILY FORTUNA: I saw,
in step 6, there's configuration stuff
for iOS, Android. Do we need to do--
is there anything. SHANNON SKIPPER: There's no-- it will do-- it inherits
from a common, normal SAN configuration. So it's not going
to match our app. We'll probably want to tweak it. EMILY FORTUNA: Ah, I see. SHANNON SKIPPER: But I think
we can make it happen-- EMILY FORTUNA: Got it. Got it. SHANNON SKIPPER: --and
then see how it looks. EMILY FORTUNA: OK, perfect. OK, so sorry, you were saying? SHANNON SKIPPER: So let's
see, so we basically define-- we defined the on-start-- we define an on-start
entry flow future. And then-- well, this
doesn't have the function that we actually
need to call, which is the one from that markdown. Let me find the-- it's the startCardEntryFlow. So it displays a
full-screen card entry view. And it takes two
callback parameters, which correspond to the
possible results of the request. So I think we should
define the pay me function and then call this-- call this-- call the
startCardEntryFlow when we-- that basically
should get us going. EMILY FORTUNA: OK. So just call-- is this
asynchronous or anything? Or are we just good? SHANNON SKIPPER: Yes, it
should be asynchronous. Let's see. EMILY FORTUNA: My question
is, do we need to wait-- or is it-- whatever. We'll wait. Start card entry flow-- possible results--
example usage. OK, on success or on cancel-- SHANNON SKIPPER:
Right, so on cancel is just if you click
the Back button. I don't-- I changed my mind. I want to add something
else to the cart. EMILY FORTUNA: OK. SHANNON SKIPPER: And then the on
success is the function where-- yeah, where we got
the nonce back. And then we want to
do some processing. So the Square In-App
Payments will validate for, like, this couldn't
possibly be a credit card, or that date for expiration
is before this one. But then you'll want
to hit the actual API with that nonce in your
payment information to make sure that
billing address and other things correspond. EMILY FORTUNA: And just for-- to clarify for
everybody, a nonce is your handle of, like,
this is the valid payment. And attach to this payment
so I can follow it through? SHANNON SKIPPER: Exactly. So what happens
is, when we call-- when we call the function to
bring up the Square payments, all the information that
the user types is securely transmitted to Square. And then Square returns,
to the In-App Payments, a nonce that stands
for those secrets. EMILY FORTUNA: All
that stuff-- got it. SHANNON SKIPPER: And so
you get back a nonce. And then you get back the
publicly available information. But that nonce is
super important, because it's what allows you
to then send that nonce along to your back end
server, and then use that to either store a
card on file or to go ahead and immediately make a payment. And it's just a
one-time use nonce. So we get this-- we basically are asking them
to put in their card secrets. And in return, we're going to
get back the secret key nonce. EMILY FORTUNA: I see. So yes, that should
give you confidence that, if you use Square
payments, you are-- the people that are writing
apps don't have access to all your credit card stuff. They just have this nonce. So they're not storing it
and potentially leaking it elsewhere. SHANNON SKIPPER:
Right, and you're not even-- you're not able to
see the secret information. So you do, then, get out
of the PCI compliance. Because it's not
available to you. EMILY FORTUNA: Got it. OK, so-- brr, brr, brr-- so what do we want to put in our
onCardEntryNonceRequestSuccess? SHANNON SKIPPER:
OK, so let's see, let me pull up the method
definition for that. onCardNonce-- so we'll want to handle-- we'll basically want to handle
two different possibilities. Yeah, you have it right there. So we'll want to take the
nonce that we got back and then validate it
against our server. Because we want to check
that the prices were right and that everything is kosher. And then we can pass that
nonce on to Square APIs. You can use whatever language
you want in the back end, use one of Square SDKs to
just like-- here's the nonce, along with the card
we'd like to add on file to the customer, or
the payment we'd like to make. And then this is going to-- if it's the happy path, then
In-App Payments complete card entry will let
you-- you can finish up. Like, we'll want to probably
show an invoice, or clear the cart, or something. You don't want to-- like, buy it again. EMILY FORTUNA:
Yeah, yeah, you're like-- if it goes into
vaporware, you're like, did it work? SHANNON SKIPPER: But
then on exception-- so if we get an exception back-- then we won't be
leaving the card. We'll stay in that card entry. And it will let
us give a message, like "that zip code
doesn't match what we have for you as a customer." EMILY FORTUNA: Yeah,
you don't want to-- you don't want to
clear the form. Pay attention, web form
creators who do that. Oh my god, it's
just so annoying. Sorry. [LAUGHING] Rant over. SHANNON SKIPPER: Yeah,
exactly, we leave them-- if we popped out
of the form, then they would have to, when
they come back in, be like, OK, I'll just change
the one-- oh no, I'll be re-entering my
whole credit card. EMILY FORTUNA: Yeah, yeah. OK, so I'm going
to just copy this. Wait, we can-- SHANNON SKIPPER: Yeah, I
think that is a good template placeholder for-- yeah, so the on
exception portion is really, what do you want to
show the user when they're not going to be leaving the
Square credit card entry? Just, hey, you can-- EMILY FORTUNA: OK, so for
starters, let's just say-- we'll just print out
"success," maybe, here. SHANNON SKIPPER: Perfect. EMILY FORTUNA: Success-- and
card details, I guess we-- SHANNON SKIPPER: Yeah,
the card details-- I think we could print
out the result.nonce. Or I guess we get
Dart type pending. So it should give us some
of the functions available. EMILY FORTUNA: OK. SHANNON SKIPPER: Nice. EMILY FORTUNA:
What's going on here? SHANNON SKIPPER:
I think it wants-- EMILY FORTUNA: Oh, is
this another import thing? Start card entry flow. At least on the
docs, I don't see-- SHANNON SKIPPER: Oh,
where startCardEntryFlow-- EMILY FORTUNA: Is defined. SHANNON SKIPPER: --is defined? Let me put the example
app real quick. Because I think it's
defined on the plug-in. Start card entry flow. EMILY FORTUNA: Oh, is
it InAppPayments dot? SHANNON SKIPPER: Exactly, EMILY FORTUNA: OK. SHANNON SKIPPER: Yeah. EMILY FORTUNA: I thought if we
imported it, it would be there. OK. All right, so-- SHANNON SKIPPER: Yeah,
that should work. EMILY FORTUNA: --we go. So there's that. And then you were
saying we can have, maybe, a snackbar or something
appear with the error message. Or what do you-- SHANNON SKIPPER: Yeah, I think-- let's see, yeah, the show
card nonce processing error gives you that thing to display. And then we can go and
customize the look of that. But yeah, this is that-- calling
that showCardProcessingError is the hook to get-- EMILY FORTUNA: It does-- oh, OK. SHANNON SKIPPER:
--to get into the-- EMILY FORTUNA: Why does
it not like message? I copied that from the thing. SHANNON SKIPPER: Does it think
that an exception doesn't have a message? EMILY FORTUNA: So it should. SHANNON SKIPPER:
It should, right? EMILY FORTUNA: On exception-- yeah. Whoops, not that-- SHANNON SKIPPER: We could
just say something in there right now, just like oops-- or actually, in the
processing error. EMILY FORTUNA: I
mean, let's just try printing out the string. Because it should--
yeah, the string version should have the error
message, I think. SHANNON SKIPPER: Perfect. EMILY FORTUNA: OK, so
now if we try paying, things should happen, yeah? SHANNON SKIPPER:
Hopefully, yeah. If we-- yes. EMILY FORTUNA: OK. Calling startCardEntryFlow
should work. Because we've set up our
Square application ID. So we told it which
Square application. So-- EMILY FORTUNA: Oh, boy. SHANNON SKIPPER:
--fingers crossed. EMILY FORTUNA: Exception. SHANNON SKIPPER: Oh. EMILY FORTUNA: OK, so
missing plug-in ID-- so OK, it doesn't like garbage. SHANNON SKIPPER: It doesn't
like the garbage entry. OK, that's-- EMILY FORTUNA: Shocking. SHANNON SKIPPER: It's
like, something is amiss. OK, so let's-- EMILY FORTUNA: Turns
out garbage is garbage. SHANNON SKIPPER: So let's
get it a real application ID. EMILY FORTUNA: OK, I'm
going to kill this. Because it'll be
unhappy with the-- SHANNON SKIPPER: OK, so I have
a bunch of application IDs. It's really easy to make one. You basically just click on
your Square developer portal. And then you just
say, new application. And then you can
click on the ID. EMILY FORTUNA: OK, so just to
tell people a little bit more, like, you go to
the Square website? SHANNON SKIPPER: Yes. So let me pull up the
[INAUDIBLE] again. So I think it's step 2 on
the guide we're following. Let's see, where is it? Lost my tabs. The Getting Started Guide-- before you start--
create a Flutter project, add In-App Payments
SDK to your project, configure a dependency, get
your Square application ID. So it's, open the
Square application dashboard, which is just our
main developer dashboard. And so I'll sign into that. And then we have-- so like, you can just create--
click New Application. We'll type our
application name, Shrine. Create Application. And here it is,
our application ID. EMILY FORTUNA: Awesome. SHANNON SKIPPER: So I
will shoot you that. EMILY FORTUNA: OK. All this will be grayed
out, because nobody wants to read my email. [HIGH TONE] All right, so we're back
with a real application ID. OK, so now-- this build failed. Oh gosh, OK, let's try again. All right, so we've
got her application ID. So that sets that
up, and is like, I'm going to make a
transaction with Shrine on the Square servers. Oh, jeez. SHANNON SKIPPER: OK,
so we have, like-- EMILY FORTUNA: Why didn't
this happen before? SHANNON SKIPPER:
I guess it didn't get far enough along to see that
there was a version mismatch. EMILY FORTUNA: Is
this in my Android's-- I'm going to pull up the iOS
simulator, just for grins. So what were you-- you
were saying [INAUDIBLE]?? SHANNON SKIPPER: I think that
that the demo, that Shrine might be pinned to a version-- EMILY FORTUNA: It could be old. SHANNON SKIPPER: --earlier than
what In-App Payments since it's a fairly new plug-in. EMILY FORTUNA: Let's move it. SHANNON SKIPPER: Yeah, can
we move the Shrine version forward? EMILY FORTUNA: Yes. SHANNON SKIPPER: Awesome. EMILY FORTUNA: So is it
this version, minSdk? Yeah. All right, yeah, we've got
16, and it says we need 21. Let's try that. SHANNON SKIPPER: Awesome. EMILY FORTUNA: We will hide
the iOS simulator for now. SHANNON SKIPPER:
Fingers crossed. EMILY FORTUNA: Oh, yes-- Android. It's weird that that
didn't happen before. SHANNON SKIPPER: Yeah, it is. EMILY FORTUNA: Go away. Seems it was full of fun
technical difficulties. So Shannon, have there
been-- have there been funny bugs in the
process of implementing this whole thing? SHANNON SKIPPER: Oh, yeah. Actually had some versioning. I was-- the other day was-- I didn't realize I was on
Flutter master and was like, it doesn't work. EMILY FORTUNA:
Too bleeding-edge. SHANNON SKIPPER:
Yeah, then I was like, oh, wait, I should probably
go to Flutter beta. And it worked. EMILY FORTUNA: Or
the release, yeah. SHANNON SKIPPER: Yeah,
really, even better-- stable. EMILY FORTUNA: While
my computer tries to blast off to the moon-- SHANNON SKIPPER: Yes. EMILY FORTUNA: You can hear it. I guess it's working on the
Android SDK version thing. OK, so it's built.
So that's better, except it's a white screen. There we go. Back to where we
were, we're going to buy an overpriced backpack. Take my money. SHANNON SKIPPER: Yay. EMILY FORTUNA: Oh, yeah. SHANNON SKIPPER: Awesome. EMILY FORTUNA: Look at that-- gorgeous. SHANNON SKIPPER: Sweet. EMILY FORTUNA: So now I can
enter some garbage numbers, and it will tell me
there's an error? SHANNON SKIPPER: Yes. Well, so actually, I'm not-- it won't-- it might not
tell you there's an error. But it should do something. EMILY FORTUNA: What should I do? Do you want to
test the success-- SHANNON SKIPPER: I
would say try out-- let's try out just trying entry. Because there are Flutter
animations out of the box that should-- OK, so that's just like
a-- so it's just like, I don't recognize that card. So if you try a
Flut-- try a 411. Or start with different
numbers, and you should-- it should auto-detect the card. Oh, wow, that's a long-- so try, like, 41111, the
canonical credit card example. Yeah, so it's going
to detect your card, if it's an AmEx, or
MasterCard, or Visa. And then-- EMILY FORTUNA: Expiration. SHANNON SKIPPER: And then it's
doing, yeah, validation here. But it's just like, this
is for possibilities. This is a permissive-- yeah, so that's too old
to be possibly valid. So it's giving me a newer date. And your CVE-- OK, so now if-- so this is where that Back
button in the upper left is the on cancel callback. EMILY FORTUNA:
Actually, I don't want to buy an overpriced backpack. SHANNON SKIPPER: Yeah, and
then the check mark is the, yes, we're going forward. And then basically, this-- the entry worked. So now it's up to
you to then take that onCardNonceRequestSuccess. Because we're going to
get the nonce back here. And then it's up to us to then-- oh nice, success, yay. EMILY FORTUNA: Success--
so now-- but why is it just spinning? SHANNON SKIPPER: That
is a good question. What do we have defined
right now under card OC? It should be calling-- oh, because we're-- EMILY FORTUNA: So it did this. SHANNON SKIPPER: So
we just need to add the bit that tells it,
inside of try, the In-App Payments complete card entry. So basically, it's
just going to-- EMILY FORTUNA: That's
just the animation? SHANNON SKIPPER: It's
just the animation. It's not going to
yield until we tell it. EMILY FORTUNA: I see. SHANNON SKIPPER: And the idea
here is that, inside that try, you get to choose if you
want to throw an exception. And then we can not
leave this modal. And to leave the modal, you
have to get into the happy path of complete card entry. EMILY FORTUNA: OK. SHANNON SKIPPER: And
then we can define the onCardEntryComplete function
for any cleanup we need to do. It's sort of the, we're
leaving the modal now. EMILY FORTUNA: OK, so
onCardEntryComplete-- SHANNON SKIPPER:
Yeah, and so we can name that-- we can name
that one arbitrarily. And it's just like, yeah,
our own business logic. It doesn't have any-- EMILY FORTUNA: It just calls it. And that does that
closing thing. SHANNON SKIPPER: So the
whole In-App Payments plug-in is basically the ability
to launch this card entry so people can
put in their card. And then we get the nonce back. And you can stay
in the card entry if there's errors that you
want to let them resolve. But then when you finish, you
get into that complete card entry, where we get to clear
a cart, or show an invoice, or whatever the user feedback we
want to give from this worked. EMILY FORTUNA: OK,
so I probably need to do a full restart
though, because-- SHANNON SKIPPER: Yes,
I think it was like, oh, we're not ever leaving this. OK, and that's awesome. So you did an anonymous
function for the print('yay'). EMILY FORTUNA: Yeah. So this is native
code here, yeah? SHANNON SKIPPER: Yes. EMILY FORTUNA: OK, so this-- hot restart-- nothing's
going to work. SHANNON SKIPPER: Yeah,
while we're in this, it's sort of like a
black box of, we-- don't look at what
the card entry is when it's not possible to. Because that way, you don't--
you're spared having to see the secret information
of the card. EMILY FORTUNA: OK,
so we will rebuild. And I can enter
that stuff again. And hopefully, it
will close our-- the dialog, as my
computer takes off again. All right, backpack added. Take my money. SHANNON SKIPPER: Yay. EMILY FORTUNA: So 411, you said? SHANNON SKIPPER: Yes, the
411 is just like the-- 4 and then ones is the signal
that this credit card is just a placeholder fake. EMILY FORTUNA: Mm, OK. SHANNON SKIPPER: OK,
so this should-- yep. Yay. So we're back. EMILY FORTUNA: Sweet. So now I imagine,
on the Flutter side, we want to return a value
that says success or whatever so that we can handle it-- handle the UI. Because this is here. We don't want to be like, why
is the backpack stil in my cart? SHANNON SKIPPER: So we'll
want to do that, probably, in the try before we do
the complete card entry. so the onCard not
success is going to be we successfully got
back from the card entry and we now have a result. EMILY FORTUNA: Yes. SHANNON SKIPPER: I think
if you do result dot-- it should be I can't remember
the name of the two-- nonce is really what
we're after here. And then so we take that
nonce and we can then send it to our back end,
and send it onto Square API to put it on a customer card-- EMILY FORTUNA: Wait, sorry, when
you say our back end this is Square's? SHANNON SKIPPER:
Square's back end, so we have SDKs in Ruby, PHB,
Java, C Sharp, et cetera, or you can use our API
directly to basically hit the new card for customer route. And you give it this nonce,
and other information you want to send to us. And then we can save a card on
file, so it just has a card ID, and so when you want
to then recharge-- if they come back and
want to shop again-- you can just charge that
card ID and customer ID. I know that-- I was poking around
in Shrine and I saw that sort of
the data back end right now is spiked out
as like a repository. It's just like a dart file
that returns the data, so we could probably do a
similar pattern of a payment service or something where
we give it this nonce and then it would
act like the server that you're going to have
the real customer data in. EMILY FORTUNA: The
server for your app-- not the Square server. SHANNON SKIPPER: Yes,
the server your apps. So the normal path
is you send whatever came from the mobile app. You send that nonce and the
information to your back end and there you can
check things like-- it's just like with a
JavaScript form-- you're like, is this a valid shopping cart? Like are we willing
to send you this? And also then you
have your secret token on your back end,
your square token. And so you're-- EMILY FORTUNA: Which is
different from the nonce? EMILY FORTUNA: It's different-- it's like an OAuth token type,
so it's your bearer token. So you keep that secret
on your server side. EMILY FORTUNA: OK. SHANNON SKIPPER:
Inside of the try, we get our nonce so we
can call a function that's going to put the card on
file or charge the card, and that will go to
your back end server-- EMILY FORTUNA: In your
back end called Square. SHANNON SKIPPER: In your
back end server called Square EMILY FORTUNA: To
actually do the charge. SHANNON SKIPPER: To actually
do the charge and then returns to your mobile app. Either the exception
saying like, hey, actually stay in this card
entry, or the success, and the complete card
entry can go forward. EMILY FORTUNA: So here's
maybe a dumb question. Say like I don't
care about like-- I don't know. I'm trying to think
of a service where-- maybe games where you're
buying pixels on your local-- say you have a game that's
entirely local, could-- I guess it would be a
similar thing to what we're going to set up here where
you just have your dart stuff and it's all stored locally. Or could you-- SHANNON SKIPPER:
The problem there-- you can make the HTTP requests,
but you can't securely store your bearer token for
using the Square ecosystem payment APIs on the mobile app. EMILY FORTUNA: On an app. SHANNON SKIPPER: Yeah, just
because of security reasons. You wouldn't want to-- EMILY FORTUNA: I see. SHANNON SKIPPER: --have people
be able to go into a debugger or get at those codes. So you just want to
put your secrets in it just like you wouldn't want
to put it in your JavaScript. It's like somebody
can dig in there. Even if you obfuscate
it, it's there. EMILY FORTUNA: Yeah. SHANNON SKIPPER: So
it's sort of better. Usually, people most
often have a backup server that's doing some functions. And then you can set up
a super simple path here. Just send it the nonce. Got to have one pathway that's
just like hit Square APIs now we have a secret key
that they can't at. EMILY FORTUNA: OK, it's obvious
that I don't do payment things. I'm like, why not keep it local? OK, cool. So as you're saying
though for this-- since we don't really have a
back end we're going to have-- SHANNON SKIPPER: We
could either extract it to a functions like a service-- we sort of have the
spiked, the fake back end for the shopping cart,
items or first even do inside of try, just
sort of like comment out either we raise an exception or
we presume it to be successful. EMILY FORTUNA: If
you're willing, let's stick it in the back end. SHANNON SKIPPER: I
think that's nice because it's where
the app currently is getting its
source of data truth, and that's where
you'd the same thing. Yeah. EMILY FORTUNA: Yeah,
so where is that? SHANNON SKIPPER:
That's a good question. OK. It's in the model directory and
it's the projects repository dot dart file and I believe. So this is where it has all of
the Vagabond stack and Stella sunglasses and Whitney
belt, et cetera. EMILY FORTUNA: Oh, my goodness
look at all those products. SHANNON SKIPPER: Oh, yeah. OK. EMILY FORTUNA: OK,
yes, all right. So would we just want
to have another class, or would we want to call it
into the product repository? Probably another class. SHANNON SKIPPER: Yeah or maybe
a payments repository or payment service. EMILY FORTUNA: Great. Payment repository. SHANNON SKIPPER: And
just like in reality, the products repository
would probably be doing an HTTP
request to a back end to fetch those products. The payment service would
really be doing an HTTP request that passes that nonce on. EMILY FORTUNA: OK, so
this would normally be on on the server
as your back end. SHANNON SKIPPER: And we
have Heroku one click deploy for spinning up a
back end, but it feels like this is
in the right place here since our source
of truth is the model. EMILY FORTUNA: Cool. OK, so what should I stick here? SHANNON SKIPPER: Oh,
that's a good question. EMILY FORTUNA: We just have a
function that's received nonce? SHANNON SKIPPER:
I think so, yeah. EMILY FORTUNA: And so we
can call this something like actually make the charge. SHANNON SKIPPER:
I like it, yeah. EMILY FORTUNA: The charge
and takes that nonce. SHANNON SKIPPER: Perfect. EMILY FORTUNA: And secrets. I can spell-- secrets here. Is there anything
we actually want to do here to help simulate-- SHANNON SKIPPER: I mean I
guess the two things you can do are not raise an error-- in which case-- EMILY FORTUNA: Done. SHANNON SKIPPER:
So we've done it. Or the other case is we
could raise an exception, and that exception message is
going to be the user feedback-- or at least how we have
it wired up right now-- is going to be the user
feedback they get inside of that card entry field. So this is like hitting the
server and us saying like-- EMILY FORTUNA: There is
a problem with your-- SHANNON SKIPPER: Yeah,
there's a problem with whatever it is that
we learned like, yes, this is a valid card. EMILY FORTUNA: Your
credit card was declined. SHANNON SKIPPER: That
would be a problem. Your credit card was
declined, or, yeah, there is a problem with
the zip or something. Maybe we can do a random
[? chant, ?] like have this function be-- EMILY FORTUNA: Oh, a
random [INAUDIBLE].. SHANNON SKIPPER: Yeah like-- EMILY FORTUNA: OK, yeah. SHANNON SKIPPER: --you
have a 10% chance of getting an exception. EMILY FORTUNA: So imports-- SHANNON SKIPPER: How
do we do a random? EMILY FORTUNA: --dart math. Let's just say random,
random close random. And random dot nextBool, and
so if random dot nextBool-- SHANNON SKIPPER:
Oh, that's fancy. EMILY FORTUNA: We can-- SHANNON SKIPPER: I love that
of instead of being like, if it's less than 0.5. EMILY FORTUNA: Yes. OK, so what do we-- just raise
an error or return a string because if this
is a server like-- SHANNON SKIPPER: It would be-- EMILY FORTUNA: --pass
the an error back-- SHANNON SKIPPER:
Yeah, it would be-- EMILY FORTUNA: --a message back. SHANNON SKIPPER: Exactly. EMILY FORTUNA: So let's have
this as a string I guess. SHANNON SKIPPER: Yeah. EMILY FORTUNA: It would be
like JSON in practice, I think. SHANNON SKIPPER: Totally, you
would be sending your HTTP request to your server, which
would be hitting Square API and then giving back an error
code that it would just be passing back to the mobile app. EMILY FORTUNA: Right, yeah,
I guess it would be a number. SHANNON SKIPPER: Right. Yeah. Or you could on your server
translate that number to the message. EMILY FORTUNA: OK, well-- SHANNON SKIPPER:
It's your choice whether you sort of
handle it on the Flutter or on the server end. EMILY FORTUNA: OK, so
credit card was declined this is not the best checkable
error messages, but you know. Return, success. Great. SHANNON SKIPPER:
Perfect, I like it. EMILY FORTUNA: So we
don't want that private. That would be sad. And then shopping carts
we're going to say, payments repository dot
actually make the charge and we have to import that. See if this works. SHANNON SKIPPER: I think
you have a missing paren. EMILY FORTUNA: You're right. There you go. And this is unhappy because-- oh, yes, we want
this to be static. Oops, get rid of products. Yeah, because there's no need to
make an object version of that. SHANNON SKIPPER: Totally. EMILY FORTUNA: Static. Oh. Yeah, yeah. Great. OK, so now we've got this
string that we would check. So our results equals that. And then we might say like if
results not equal success-- SHANNON SKIPPER: Nice. EMILY FORTUNA: --then we
would throw our error, yeah? SHANNON SKIPPER: Exactly. EMILY FORTUNA: So throw new-- I'm just going to do error and
I'm going to pass the result. SHANNON SKIPPER: Is error
the standard error class? EMILY FORTUNA: Yeah. SHANNON SKIPPER: OK. EMILY FORTUNA: Oh, wait. Yes. OK, so yes but you can't
pass a message to it. We need a more specific error. SHANNON SKIPPER: Gotcha. EMILY FORTUNA: How about-- SHANNON SKIPPER: One that takes
an error with an error message. EMILY FORTUNA: Yeah, this
argument error is not good. What else is good? Where are subclasses? OK, AssertionError, UnsupportedError,
RemoteError maybe? Let's call it RemoteError. SHANNON SKIPPER:
It's remote of sorts. EMILY FORTUNA:
Yeah, let's do that. RemoteError. SHANNON SKIPPER: It didn't
happen on this mobile device. EMILY FORTUNA: OK. Why is it not like response? Oh, because we did it again. Results-- chargeResults. OK. What is this? Oh, gosh. Maybe this is the
wrong stat description? OK, we don't want that. New error. StateError? It's not the best, but
we'll call it that. Technically, we
should define our own, but we're not going
to do that right now. OK, cool, so that's
all implemented. SHANNON SKIPPER: Nice. EMILY FORTUNA: Now we
say, take my money, and we have to do this. Oops, no W's in the zipcode. Oh my gosh. SHANNON SKIPPER: Oh,
I guess it doesn't balk at that because it's
like some zip codes might. EMILY FORTUNA: Sure. SHANNON SKIPPER: Well,
depends on the country. EMILY FORTUNA: OK, so-- SHANNON SKIPPER: So
I expected that to-- EMILY FORTUNA: OK, so I think
part of the problem is I need to do a hot restart
because I declared this completely new class. If you have stuff that-- but and because this
is the native code-- I'm going to just kill the app. SHANNON SKIPPER: Nice. EMILY FORTUNA: OK, try again. SHANNON SKIPPER: So when
you create a new file it's like it's hot reloading
files that already exist. EMILY FORTUNA: Yeah,
basically, anytime you define like a
completely new object-- like hot restart-- you can add
things to existing objects, but it can't just like create
new objects out of thin air. SHANNON SKIPPER: Gotcha. EMILY FORTUNA: So, yeah. So tell us more about-- while
this is loading up-- fun in developing Square
plugins or fun Square bugs. SHANNON SKIPPER:
Yeah, absolutely. So this is the
in-app payments where you want to have users pay
on their own mobile device. And so we also
have fun where you can your mobile app
with our hardware, so that's the Reader SDK. EMILY FORTUNA: Right. SHANNON SKIPPER: So that one
has a lot of fun you're-- you, actually, on that one just
tell it the amount currency and that's it. It then wakes the Reader up via
Bluetooth and the user pays. And the SDK itself
sends the nonce along with the amount in currency, and
does the full round trip back to your app. So it's designed a
little different. The in-app payments is really
about popping this user interaction and giving
you back a nonce to handle it the way you like. Whereas the in-app
payments is about using our Square hardware, the little
dongle or the various readers. So that's definitely been fun
to make the multiple Flutter plugins. EMILY FORTUNA: Nice. This is still hanging
so when all else fails, use print debugging. SHANNON SKIPPER: Nice. EMILY FORTUNA: Guess
we don't need this. SHANNON SKIPPER: So it's like,
was our payments repo called? EMILY FORTUNA: Yeah. Why would this be-- so the card entry flow
try actually charge. SHANNON SKIPPER: It should work. Results nonce is just a string. EMILY FORTUNA: We could
also use break points, but old habits die hard. SHANNON SKIPPER: Oh, print
based debugging is the best. EMILY FORTUNA: OK. SHANNON SKIPPER: Got our card. EMILY FORTUNA: Sure. About to make the charge. SHANNON SKIPPER: OK,
so it got and printed a bug to make the call or no? EMILY FORTUNA: Yeah. Is it-- Did we get here? SHANNON SKIPPER: Good question. EMILY FORTUNA: This
is the downside of having this thing
be in native view is we have to keep
restarting if we're trying to debug this part. SHANNON SKIPPER: Yes,
that's the prickly portion. EMILY FORTUNA: Let's see here,
if charge result does not equal success throw our
poorly used StateError. We complete card
entry to close it. Otherwise, if there is an
exception, which there should be, then we should see that. SHANNON SKIPPER: Yes,
maybe it is working and exception [? to ?]
string because it should be showing us the issue. It shouldn't be leaving the
model if there is an exception. So I guess-- EMILY FORTUNA: But we should
also see this print statement. SHANNON SKIPPER: Oh, we
totally should you're right. No, you're right. Yeah. EMILY FORTUNA: So there's
something strange going on here. 411. SHANNON SKIPPER: To get
to the point of the one. That's where you
switch to the twos. EMILY FORTUNA: You
know you guys need-- SHANNON SKIPPER: What? EMILY FORTUNA: -is a-- to be able to pass
debug or something. Where it's like auto-populated
with this like-- SHANNON SKIPPER:
That would be good. EMILY FORTUNA: --garbage. SHANNON SKIPPER:
That would be good. Save you from guessing
41111-- one key. Hey, OK, it liked that one. So that was a yay. EMILY FORTUNA: Yeah. SHANNON SKIPPER: That could
be that we [? are ?] role. Maybe it's not working
an exception or maybe the starting over fixed it. EMILY FORTUNA: So-- SHANNON SKIPPER: Randomness. EMILY FORTUNA: Yeah,
try one more time just to see if we can,
actually, oh, it's in the And I can't restart. I was going to say, let's
force it to take this, but we will keep going. About to make the charge. SHANNON SKIPPER: But it's not-- EMILY FORTUNA: I need to put-- I bet it's hitting this. Why is it failing
so spectacularly, not going to our error? That's the question. SHANNON SKIPPER: It doesn't seem
like there's anything happening that should hang it. EMILY FORTUNA:
Let's look at our-- maybe-- SHANNON SKIPPER: Is this-- EMILY FORTUNA:
Maybe because we're catching on exceptions,
specifically, rather than errors as well. SHANNON SKIPPER: StateError
maybe not an exception. EMILY FORTUNA: Yes,
I bet that's it. Yeah. Yeah, so exceptions
versus errors. In fact, maybe I should
be using exception, but I think we also
shouldn't be saying we should catch on anything. SHANNON SKIPPER: Nice. EMILY FORTUNA: So I'm
just going to say, catch. SHANNON SKIPPER: Got
fewer characters. EMILY FORTUNA: Yes, and
maybe we'll stick with error, I think. Well, OK-- what do
we want to call this? We'll call it-- IO. No, it's not [? IOexception. ?] We'll stick with
error, but yeah, I bet that's exactly the problem. So we will step through
this one more time. SHANNON SKIPPER: Yay. OK, so that's the way
that you can communicate without the user having to-- EMILY FORTUNA: Perfect. SHANNON SKIPPER: --redo. Awesome. EMILY FORTUNA: And so
then you can go back and you got your thing. Yay! SHANNON SKIPPER:
Yay, nicely done. That's awesome. EMILY FORTUNA: OK, so let me
take out my print statements. If we have just one more
little thing we can do-- I think on the success we
should clear this guy out. We should clear our cart. SHANNON SKIPPER: Yes. EMILY FORTUNA: Should we return
here like a Boolean success failure here? Rule? SHANNON SKIPPER: Yeah,
that's a good question. Yeah. EMILY FORTUNA: And then we
can call model dot clearCart. SHANNON SKIPPER: I think
it might be even nicer to-- where we have the print yay-- just have it call a function. EMILY FORTUNA:
Model dot clearCart. SHANNON SKIPPER: Exactly, yeah. EMILY FORTUNA: OK. AppStateModel, and I know
I'm abusing this model. I apologize, people. Model clearCart. Great, OK. SHANNON SKIPPER: Could
we even give it clearCart as the on CartEntry complete? EMILY FORTUNA: Oh,
did I mess up this? StateModel. Sorry, you were saying? SHANNON SKIPPER:
Oh, I was wondering if we could give it the
name of the function to call for our on CartEntry complete? EMILY FORTUNA: Oh, my gosh. Yes, please. Thank you. SHANNON SKIPPER: And get rid
of the [? non ?] assumption. EMILY FORTUNA: Yeah. SHANNON SKIPPER: Oh, and
could we omit the model dot and have it just be clearCart? EMILY FORTUNA: I think we need-- SHANNON SKIPPER: We need it. EMILY FORTUNA: --the
clearCart needs to be accessed
through the model. SHANNON SKIPPER: Oh, right. Of course, yeah. EMILY FORTUNA: OK,
so add our bag. Whoa. App state model dynamic
is not a subtype. Oh, payment. Did I-- but I thought I
was very vague on what my actual function could be. Oh, OK. Yep. All right, so the problem
is [? pretty ?] button is expecting this-- just call this with
a no parameters, but this payment thing has this. So I'm going to do
a quick fix here. Pass and model-- I have to make this a function. That other function
that are passing in-- we will just not use. We'll use underscore. SHANNON SKIPPER: Nice. EMILY FORTUNA: OK, and this
should be the last thing. 41111111. OK, that's-- can we try again? SHANNON SKIPPER: Awesome. EMILY FORTUNA: Well done, so we
have added in that payment flow to our app. SHANNON SKIPPER:
And then we could-- with just some configuration--
like match the color, match the font,
effect the error color in those models and everything. EMILY FORTUNA: Nice, and
that's on these pages here. SHANNON SKIPPER: Yes, there's
one in particular that has a nice graphic of the-- EMILY FORTUNA: How do
we get to that page? SHANNON SKIPPER: It is in the
in-app payments SDK cookbook. I sent a link to your-- EMILY FORTUNA: App payments. SHANNON SKIPPER: And it's
the customized payment form. EMILY FORTUNA: Oh,
I think I saw that. SHANNON SKIPPER: And
it's nice because instead of a bunch of text stuff, you
get the picture of the form with arrows showing you what
to set for each of the-- EMILY FORTUNA: Nice. SHANNON SKIPPER: Yep, that's it. EMILY FORTUNA: Cool. Awesome. SHANNON SKIPPER: OK. EMILY FORTUNA: Well,
thank you, Shannon. SHANNON SKIPPER:
Thank you so much. EMILY FORTUNA: Is there
any last parting words you want to share with people? SHANNON SKIPPER: No, I think
consider Square for online, in person, or in-app
payments and definitely try the in-app
payments with Flutter. EMILY FORTUNA: Awesome. Thanks for showing
us how it works. We will see you
again in two weeks. [MUSIC PLAYING]