I am using the Braintree for Payment Process. I have created a subscrition plan and I am using PayPal and Card Payment in My Site. I was able to create a Successful subscription, But I want to show the get the response when the subscription is active/canceled and payment done or not. I know this can be done by webhooks But I am not getting where to start. I am using the laravel Framework.
My Client Side code:
<form id="cardForm">
<div class="panel">
<header class="panel__header">
<h1>Card Payment</h1>
</header>
<div class="panel__content">
<div class="textfield--float-label">
<label class="hosted-field--label" for="card-number"><span class="icon">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path
d="M20 4H4c-1.11 0-1.99.89-1.99 2L2 18c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V6c0-1.11-.89-2-2-2zm0 14H4v-6h16v6zm0-10H4V6h16v2z"/>
</svg></span> Card Number
</label>
<div id="card-number" class="hosted-field"></div>
</div>
<div class="textfield--float-label">
<label class="hosted-field--label" for="expiration-date">
<span class="icon">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24">
<path
d="M9 11H7v2h2v-2zm4 0h-2v2h2v-2zm4 0h-2v2h2v-2zm2-7h-1V2h-2v2H8V2H6v2H5c-1.11 0-1.99.9-1.99 2L3 20c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V9h14v11z"/>
</svg>
</span>
Expiration Date</label>
<div id="expiration-date" class="hosted-field"></div>
</div>
<div class="textfield--float-label">
<label class="hosted-field--label" for="cvv">
<span class="icon">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24">
<path
d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/>
</svg>
</span>
CVV</label>
<div id="cvv" class="hosted-field"></div>
</div>
</div>
<footer class="panel__footer">
<button class="pay-button">Submit</button>
</footer>
</div>
</form>
<script>
var paypalButton = document.querySelector('#paypal');
var cardButton = document.querySelector('.pay-button');
var CLIENT_AUTHORIZATION = $('#client').val();
console.log(CLIENT_AUTHORIZATION);
braintree.client.create({
authorization: CLIENT_AUTHORIZATION
}, function (clientErr, clientInstance) {
if (clientErr) {
console.error('Error creating client:', clientErr);
return;
}
braintree.paypal.create({
client: clientInstance
}, function (paypalErr, paypalInstance) {
if (paypalErr) {
console.error('Error creating PayPal:', paypalErr);
return;
}
paypalButton.removeAttribute('disabled');
paypalButton.addEventListener('click', function (event) {
paypalInstance.tokenize({
flow: 'vault',
enableShippingAddress: false,
shippingAddressEditable: false,
}, function (tokenizeErr, payload) {
if (tokenizeErr) {
if (tokenizeErr.type !== 'CUSTOMER') {
console.error('Error tokenizing:', tokenizeErr);
alert(tokenizeErr.message);
}
return;
}
paypalButton.setAttribute('disabled', true);
console.log('Got a nonce! You should submit this to your server.');
console.log(payload.nonce);
$.ajax({
url: '/subscription',
type: 'post',
data: {
nonce: payload.nonce,
payload: payload
},
success: function (response) {
console.log(response);
},
error: function (response, status, err) {
}
});
});
}, false);
});
braintree.hostedFields.create({
client: clientInstance,
styles: {
'input': {
'font-size': '16px',
'font-family': 'roboto, verdana, sans-serif',
'font-weight': 'lighter',
'color': 'black'
},
':focus': {
'color': 'black'
},
'.valid': {
'color': 'black'
},
'.invalid': {
'color': 'red'
}
},
fields: {
number: {
selector: '#card-number',
placeholder: '1111 1111 1111 1111'
},
cvv: {
selector: '#cvv',
placeholder: '111'
},
expirationDate: {
selector: '#expiration-date',
placeholder: 'MM/YY'
},
}
}, function (err, hostedFieldsInstance) {
if (err) {
console.error(err);
return;
}
hostedFieldsInstance.on('focus', function (event) {
var field = event.fields[event.emittedBy];
$(field.container).next('.hosted-field--label').addClass('label-float').removeClass('filled');
});
hostedFieldsInstance.on('blur', function (event) {
var field = event.fields[event.emittedBy];
if (field.isEmpty) {
$(field.container).next('.hosted-field--label').removeClass('label-float');
} else if (event.isValid) {
$(field.container).next('.hosted-field--label').addClass('filled');
} else {
$(field.container).next('.hosted-field--label').addClass('invalid');
}
});
hostedFieldsInstance.on('empty', function (event) {
var field = event.fields[event.emittedBy];
$(field.container).next('.hosted-field--label').removeClass('filled').removeClass('invalid');
});
hostedFieldsInstance.on('validityChange', function (event) {
var field = event.fields[event.emittedBy];
if (field.isPotentiallyValid) {
$(field.container).next('.hosted-field--label').removeClass('invalid');
} else {
$(field.container).next('.hosted-field--label').addClass('invalid');
}
});
cardButton.removeAttribute('disabled');
$('#cardForm').submit(function (event) {
event.preventDefault();
hostedFieldsInstance.tokenize(function (err, payload) {
if (err) {
$('.panel__header > h1').html(err.message);
$('.panel__header').css("background", "#D50000 none repeat scroll 0 0");
return;
}
cardButton.setAttribute('disabled', true);
$('.pay-button').html("Please Wait ..");
$.ajax({
url: '/subscription',
type: 'post',
data: {
nonce: payload.nonce,
payload: payload
},
success: function (response) {
console.log(response);
},
error: function (response, status, err) {
}
});
});
});
});
});
</script>
My server side code:
public function dropin()
{
$clientToken = \Braintree_ClientToken::generate();
return view('User::user.dropui')->with("client", $clientToken);
}
public function subscription(Request $request)
{
$result = Braintree_Customer::create(array(
'email' => 'alokchaturvedi#globussoft.in',
'firstName' => 'Aloknotrail ',
'lastName' => 'Kumar',
));
if($result->success == 1){
//print_r($result->customer->id);
$resultcreate = Braintree_PaymentMethod::create([
'customerId' => $result->customer->id,
'paymentMethodNonce' => $request->nonce,
'options' => [
'failOnDuplicatePaymentMethod' => true
]
]);
if($resultcreate->success == 1){
//print_r($resultcreate->paymentMethod->token);
$resultSubscription = Braintree_Subscription::create([
'paymentMethodToken' => $resultcreate->paymentMethod->token,
'planId' => 'pro'
]);
}else{
dd($resultcreate);
}
}
dd($resultSubscription);
}
Full disclosure: I work at Braintree. If you have any further questions, feel free to contact support.
To create webhooks to track specific changes in subscriptions and transactions, go to the Braintree Control Panel and assign specific triggers to endpoints on your server. When an action occurs (like a transaction being settled), your endpoint will be called with the POST parameters of bt_signature and bt_payload. An example of creating a webhookNotification which contains information about the event, is below:
$webhookNotification = Braintree_WebhookNotification::parse(
$_POST["bt_signature"], $_POST["bt_payload"]
);
$webhookNotification->kind;
# => "subscription_went_past_due"
$webhookNotification->timestamp;
# => Sun Jan 1 00:00:00 UTC 2016
For more information on webhooks, please check out this documentation.
Webhooks in general are routes in your application that can accept API requests from the payment provider to perform action accordingly.
So in order to start, you need first to define the route, for example:
Route::get('paypal/webhook', 'WebhookController#handle');
Then you create the new WebhookController, that should be responsible for accepting the requests and responds accordingly.
class WebhookController extends Controller
{
public function handle(Request $request)
{
//handle request here
}
}
In your Braintree settings you should set the webhook endpoint so their API can consume it.
Hope that helps.
Related
I handle vuejs + laravel
I Controller :
public function listData (Request $request)
{
$currentPage = !empty($request->currentPage) ? $request->currentPage : 1;
$pageSize = !empty($request->pageSize) ? $request->pageSize : 30;
$skip = ($currentPage - 1) * $pageSize;
$totalProduct = Product::select(['id', 'name'])->get();
$listProduct = Product::select(['id', 'name'])
->skip($skip)
->take($pageSize)
->get();
return response()->json([
'listProduct' => $listProduct,
'total' => $totalProduct,
]);
}
In vuejs
data() {
return {
pageLength: 30,
columns: [
{
label: "Id",
field: "id",
},
{
label: "Name",
field: "name",
},
],
total: "",
rows: [],
currentPage: 1,
};
},
created() {
axios
.get("/api/list")
.then((res) => {
this.rows = res.data. listProduct;
this.total = res.data.total;
})
.catch((error) => {
console.log(error);
});
},
methods: {
changePagination() {
axios
.get("/api/list", {
params: {
currentPage: this.currentPage,
pageSize: this.pageLength,
},
})
.then((res) => {
this.rows = res.data. listProduct;
this.total = res.data.total;
})
.catch((error) => {
console.log(error);
});
},
},
Template :
<vue-good-table
:columns="columns"
:rows="rows"
:rtl="direction"
:search-options="{
enabled: true,
externalQuery: searchTerm,
}"
:select-options="{
enabled: false,
selectOnCheckboxOnly: true,
selectionInfoClass: 'custom-class',
selectionText: 'rows selected',
clearSelectionText: 'clear',
disableSelectInfo: true,
selectAllByGroup: true,
}"
:pagination-options="{
enabled: true,
perPage: pageLength,
}"
>
<template slot="pagination-bottom">
<div class="d-flex justify-content-between flex-wrap">
<div class="d-flex align-items-center mb-0 mt-1">
<span class="text-nowrap"> Showing 1 to </span>
<b-form-select
v-model="pageLength"
:options="['30', '50', '100']"
class="mx-1"
#input="changePagination"
/>
<span class="text-nowrap"> of {{ total }} entries </span>
</div>
<div>
<b-pagination
:value="1"
:total-rows="total"
:per-page="pageLength"
first-number
last-number
align="right"
prev-class="prev-item"
next-class="next-item"
class="mt-1 mb-0"
v-model="currentPage"
#input="changePagination"
>
<template #prev-text>
<feather-icon icon="ChevronLeftIcon" size="18" />
</template>
<template #next-text>
<feather-icon icon="ChevronRightIcon" size="18" />
</template>
</b-pagination>
</div>
</div>
</template>
I am dealing with a product list that has 500,000 products. I don't want to take it out once. I want it to pull out 30 products each time, when I click on the partition, it will call to the api to call the next 30 products.. But my problem is the default pageLength is 30 products, When I choose show showing 50 products , it still shows 30 products on the page list (But I console.log (res.data.listProduct)) it shows 50 products, how do I change the default value pageLength.
Is there any way to fix this, Or am I doing something wrong. Please advise. Thanks.
Add this into computed =>
paginationOptionsComputed(){
return { enabled: true, perPage: Number(this.pageLength), }
}
And change :pagination-options="paginationOptionsComputed"
Note: actual problem is that vue-good-table expects perPage as number. If you look at the initializePagination method in here you can see this:
if (typeof perPage === 'number') {
this.perPage = perPage;
}
I am doing the search engine section in VueJS and Laravel, but I have a problem that does not allow me to advance in the other sections. The search engine opens and everything but when I write it only sends the first letter or 2 but not all of them like this in this image:
image of the data you send
the data that I write
After that it shows me the following error in console:
Uncaught (in promise) NavigationDuplicated: Avoided redundant navigation to current location: "/search?q=th"
Now showing my search engine code:
<template>
<div class="form_MCycW">
<form autocomplete="off" #sumbit.prevent>
<label class="visuallyhidden" for="search">Search</label>
<div class="field_2KO5E">
<input id="search" ref="input" v-model.trim="query" name="search" type="text" placeholder="Search for a movie, tv show or person..." #keyup="goToRoute" #blur="unFocus">
<button v-if="showButton" type="button" aria-label="Close" #click="goBack">
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 15 15"><g fill="none" stroke="#fff" stroke-linecap="round" stroke-miterlimit="10" stroke-width="1.5"><path d="M.75.75l13.5 13.5M14.25.75L.75 14.25"/></g></svg>
</button>
</div>
</form>
</div>
</template>
<script>
import { mapState } from 'vuex';
export default {
data() {
return {
query: this.$route.query.q ? this.$route.query.q : ''
}
},
computed: {
showButton() {
return this.$route.name === 'search';
},
...mapState({
search: state => state.event.fromPage
})
},
mounted() {
this.$refs.input.focus();
},
methods: {
goToRoute() {
if (this.query) {
this.$router.push({
name: 'search',
query: { q: this.query },
});
} else {
this.$router.push({
path: this.fromPage,
});
}
},
goBack() {
this.query = '';
this.$router.push({
path: '/',
});
},
unFocus (e) {
if (this.$route.name !== 'search') {
const target = e.relatedTarget;
if (!target || !target.classList.contains('search-toggle')) {
this.query = '';
this.$store.commit('closeSearch');
}
}
}
}
}
</script>
This is the other section of the search engine:
<template>
<main class="main">
<div class="listing">
<div class="listing__head"><h2 class="listing__title">{{ title }}</h2></div>
<div class="listing__items">
<div class="card" v-for="(item, index) in data.data" :key="index">
<router-link :to="{ name: 'show-serie', params: { id: item.id }}" class="card__link">
<div class="card__img lazyloaded"><img class="lazyload image_183rJ" :src="'/_assets/img/covers/posters/' + item.poster" :alt="item.name"></div>
<h2 class="card__name">{{ item.name }}</h2>
<div class="card__rating">
<div class="card__stars"><div :style="{width: item.rate * 10 + '%'}"></div></div>
<div class="card__vote">{{ item.rate }}</div>
</div>
</router-link>
</div>
</div>
</div>
</main>
</template>
<script>
import { mapState } from 'vuex';
let fromPage = '/';
export default {
name: "search",
metaInfo: {
bodyAttrs: {
class: 'page page-search'
}
},
computed: {
...mapState({
data: state => state.search.data,
loading: state => state.search.loading
}),
query() {
return this.$route.query.q ? this.$route.query.q : '';
},
title() {
return this.query ? `Results For: ${this.query}` : '';
},
},
async asyncData ({ query, error, redirect }) {
try {
if (query.q) {
this.$store.dispatch("GET_SEARCH_LIST", query.q);
} else {
redirect('/');
}
} catch {
error({ message: 'Page not found' });
}
},
mounted () {
this.$store.commit('openSearch');
this.$store.commit('setFromPage', fromPage);
if (this.data.length == 0 || this.data === null) {
this.$store.dispatch("GET_SEARCH_LIST", this.query);
}
setTimeout(() => {
this.showSlideUpAnimation = true;
}, 100);
},
beforeRouteEnter (to, from, next) {
fromPage = from.path;
next();
},
beforeRouteUpdate (to, from, next) {
next();
},
beforeRouteLeave (to, from, next) {
const search = document.getElementById('search');
next();
if (search && search.value.length) {
this.$store.commit('closeSearch');
}
}
};
</script>
In my routes section it is defined as follows:
{
name: 'search',
path: '/search',
component: require('../views/' + themeName + '/control/search/index').default
}
It is supposed to be a real-time search engine. I would appreciate your help in solving this problem...
What you need is a debounce. What it does is that it wait or delay till the user had finished typing before the model get updated or before you send it to the server.
An example of how it works is here
Here is a package for it.
https://github.com/vuejs-tips/v-debounce
I'm trying to return the errors in server side validation so the user can know which error they have, but I don't know how to return something that it is understandable for a normal person.
Here's my front-end
<v-form>
<v-row>
<v-col cols="12" sm="6" md="6">
<v-text-field label="Serial Number" v-model="plane.serial_number" color="black" counter="30"></v-text-field>
</v-col>
</v-row>
<v-btn color="yellow" class="black-text" #click="add()">Submit</v-btn>
</v-form>
<script>
import Swal from 'sweetalert2'
export default {
data() {
return {
errors: [],
plane: {
serial_number: '',
},
}
},
methods: {
add() {
const params = {
serial_number: this.plane.serial_number,
};
axios.post(`/planes`, params)
.then(res => {
Swal.fire({
title: 'Success!',
html: 'Plane created successfully!',
icon: 'success',
confirmButtonText: 'OK',
})
}).catch(e => {
this.errors = e;
console.log(this.errors);
Swal.fire({
title: 'Error!',
icon: 'error',
})
})
},
}
}
</script>
Back-end
public function store(Request $request)
{
$this->validate($request, [
'serial_number' => ['required','string', 'unique:airplanes']
]);
$airplane = new Airplane();
$airplane->serial_number = $request->serial_number;
$airplane->save();
}
The console.log isn't returning anything at all.
In this.errors if you output this.errors.message you should see a friendly message. Hope that helps! ... You can also dig into the this.errors.response object which has things like the headers and status.
I have made a CRUD based admin pannel with laravue. Now my problem is that whenever I am on the update page, I can't change the category immediately. I always have to make change in some other field to view the changed category. Its working fine on add card page when all the fields are empty.
This is the video of my issue
https://thisisntmyid.tumblr.com/post/189075383873/my-issue-with-elementui-and-laravue-and-vue
I've included the code for my list page as well as the add/update page.
Code for the listing page that will lead u to edit page
<div class="app-container">
<h1>My Cards</h1>
<Pagination
:total="totalCards"
layout="total, prev, pager, next"
:limit="10"
:page.sync="currentPage"
#pagination="loadNewPage"
></Pagination>
<el-table v-loading="loadingCardsList" :data="cards" stripe style="width: 100%">
<el-table-column prop="name" sortable label="Product Name"></el-table-column>
<el-table-column prop="description" label="Description" width="200px"></el-table-column>
<el-table-column prop="price" label="Price"></el-table-column>
<el-table-column prop="cardcategory" sortable label="Category"></el-table-column>
<el-table-column label="Operations" width="300px">
<template slot-scope="scope">
<el-button size="mini" type="primary" #click="handleView(scope.$index, scope.row)">View</el-button>
<el-button size="mini" #click="handleEdit(scope.$index, scope.row)">Edit</el-button>
<el-button size="mini" type="danger" #click="handleDelete(scope.$index, scope.row)">Delete</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog :visible.sync="viewCard" width="30%">
<el-card class="box-card">
<h1>{{currentViewedCard.name}}</h1>
<p>{{currentViewedCard.description}}</p>
<p>{{currentViewedCard.price}}</p>
<el-tag>{{currentViewedCard.cardcategory}}</el-tag>
</el-card>
</el-dialog>
</div>
</template>
<script>
import Resource from '#/api/resource';
import Pagination from '#/components/Pagination/index.vue';
const cardcategoryResource = new Resource('cardcategories');
const cardResource = new Resource('cards');
export default {
name: 'Cards',
components: {
Pagination,
},
data() {
return {
cards: [],
categories: [],
currentPage: '',
totalCards: '',
loadingCardsList: true,
currentViewedCard: '',
viewCard: false,
};
},
created() {
this.getCardCategories();
this.getCardList({ page: 1 });
},
methods: {
async getCardList(query) {
this.loadingCardsList = true;
const data = await cardResource.list(query);
this.cards = data.data;
for (const card of this.cards) {
card['cardcategory'] = this.getCategoryName(card.cardCategory_id);
}
console.log(this.cards);
this.totalCards = data.total;
this.loadingCardsList = false;
},
async getCardCategories() {
this.categories = await cardcategoryResource.list({});
console.log(this.categories);
},
loadNewPage(val) {
this.getCardList({ page: val.page });
},
getCategoryName(id) {
return this.categories[id - 1].name;
},
handleView(index, info) {
this.viewCard = true;
this.currentViewedCard = info;
},
handleEdit(index, info) {
this.$router.push('/cards/edit/' + info.id);
},
closeDialog() {
this.viewProduct = false;
this.currentProductInfo = null;
},
handleDelete(index, info) {
cardResource.destroy(info.id).then(response => {
this.$message({
message: 'Card Deleted Successfully',
type: 'success',
duration: 3000,
});
this.getCardList({ page: this.currentPage });
});
},
},
};
</script>
<style lang="scss" scoped>
</style>
Code for the add/update page
<template>
<div class="app-container">
<el-form ref="form" :model="formData" label-width="120px">
<el-form-item label="Name">
<el-input v-model="formData.name"></el-input>
</el-form-item>
<el-form-item label="Description">
<el-input type="textarea" v-model="formData.description"></el-input>
</el-form-item>
<el-form-item label="Price">
<el-input v-model="formData.price"></el-input>
</el-form-item>
<el-form-item label="Category">
<!-- DO something here like binding category id to category name sort of meh... -->
<el-select v-model="formData.cardcategory" placeholder="please select category">
<el-option v-for="item in categories" :key="item" :label="item" :value="item"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" #click="editable ? updateProduct() : createProduct()">Save</el-button>
<el-button #click="editable ? populateFormData($route.params.id) : formDataReset()">Reset</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import Resource from '#/api/resource';
const cardcategoryResource = new Resource('cardcategories');
const cardResource = new Resource('cards');
export default {
data() {
return {
formData: {
name: '',
description: '',
price: '',
cardcategory: '',
},
categories: [],
editable: '',
};
},
created() {
this.getCategories();
if (this.$route.params.id) {
this.editable = true;
this.populateFormData(this.$route.params.id);
} else {
this.editable = false;
}
},
methods: {
async getCategories() {
this.categories = (await cardcategoryResource.list({})).map(
cat => cat.name
);
console.log(this.categories);
},
formDataReset() {
this.formData = {
name: '',
description: '',
price: '',
cardcategory: '',
};
},
populateFormData(id) {
cardResource.get(id).then(response => {
this.formData = Object.assign({}, response);
this.formData.price = this.formData.price.toString();
this.formData.cardcategory = this.categories[
this.formData.cardCategory_id - 1
];
delete this.formData.cardCategory_id;
});
},
filterFormData(formData) {
const cardData = Object.assign({}, formData);
cardData['cardCategory_id'] =
this.categories.indexOf(cardData.cardcategory) + 1;
delete cardData.cardcategory;
return cardData;
},
createProduct() {
const cardData = this.filterFormData(this.formData);
cardResource
.store(cardData)
.then(response => {
this.$message({
message: 'New Card Added',
type: 'success',
duration: 3000,
});
this.formDataReset();
})
.catch(response => {
alert(response);
});
},
updateProduct() {
const cardData = this.filterFormData(this.formData);
cardResource.update(this.$route.params.id, cardData).then(response => {
this.$message({
message: 'Card Updated Successfully',
type: 'success',
duration: 3000,
});
this.populateFormData(this.$route.params.id);
});
},
},
};
</script>
<style>
</style>
Update
here is the git hub repo for my project
https://github.com/ThisIsntMyId/laravue-admin-pannel-demo
I'm using Vuejs to display data from an API to a template. I'm trying to figure out why I am not returning data for the team so I can display in for the template. Right now when I use the VueJS Chrome Extention it shows that the team is an empty object.
<div v-if="team">
<div class="row">
<div class="col-12 col-sm-12">
<div class="fw-700 pb3 mb5" style="border-bottom: 1px solid #333;">Name:</div>
<div class="mb10" style="font-size: 20px;">{{ team.name }}</div>
</div>
</div>
<script>
module.exports = {
http: {
headers: {
'X-CSRF-TOKEN': window.Laravel.csrfToken
}
},
props: [ 'id', 'editable' ],
data: function(){
return {
modalName: 'additionalInfo',
team:{
}
}
};
},
methods: {
fetchInfo: function(){
this.$http.get('/api/teams/info', { params: { id: this.id } }).then((response) => {
this.team = response.data;
});
},
},
}
}
</script>
It is empty because your method fetchInfo isn't being called, so you need to do something like this:
created () {
this.fetchInfo()
}
This will call the function fetchInfo which in turn will fetch and fill this.team